2

Based on a previous question I had recently posted: How to create lines between nearby particles in ThreeJS?

I was able to create individual lines joining nearby particles. However, the lines are being drawn twice because of the logic of the particle system. This is because of how the original 2D particle system worked: https://awingit.github.io/particles/

This also draws the lines twice. For each pair of particles connecting a line, the line is drawn.

I do not think this is ideal for performance. How would I only draw a line once for each joining points?

P.S. Here is exactly the effect I would like to achieve, but cannot make sense of the code: http://freelance-html-developer.com/clock/

I would like to understand the fundamental logic.

UPDATE:

I have created a jsfiddle with my progress.

    var canvas, canvasDom, ctx, scene, renderer, camera, controls, geocoder, deviceOrientation = false;
    var width = 800,
        height = 600;
    var particleCount = 20;


    var pMaterial = new THREE.PointsMaterial({
        color: 0x000000,
        size: 0.5,
        blending: THREE.AdditiveBlending,
        //depthTest: false,
        //transparent: true
    });
    var particles = new THREE.Geometry;
    var particleSystem;
    var line;
    var lines = {};
    var lineGroup = new THREE.Group();
    var lineMaterial = new THREE.LineBasicMaterial({
        color: 0x000000,
        linewidth: 1
    });

    var clock = new THREE.Clock();
    var maxDistance = 15;

    function init() {
        canvasDom = document.getElementById('canvas');
        setupStage();
        setupRenderer();
        setupCamera();
        setupControls();
        setupLights();
        clock.start();
        window.addEventListener('resize', onWindowResized, false);
        onWindowResized(null);
        createParticles();
        scene.add(lineGroup);
        animate();
    }

    function setupStage() {
        scene = new THREE.Scene();
    }

    function setupRenderer() {
        renderer = new THREE.WebGLRenderer({
            canvas: canvasDom,
            logarithmicDepthBuffer: true
        });
        renderer.setSize(width, height);
        renderer.setClearColor(0xfff6e6);
    }

    function setupCamera() {
        camera = new THREE.PerspectiveCamera(70, width / height, 1, 10000);
        camera.position.set(0, 0, -60);
    }

    function setupControls() {
        if (deviceOrientation) {
            controls = new THREE.DeviceOrientationControls(camera);
            controls.connect();
        } else {
            controls = new THREE.OrbitControls(camera, renderer.domElement);
            controls.target = new THREE.Vector3(0, 0, 0);
        }
    }

    function setupLights() {
        var light1 = new THREE.AmbientLight(0xffffff, 0.5); // soft white light
        var light2 = new THREE.PointLight(0xffffff, 1, 0);

        light2.position.set(100, 200, 100);

        scene.add(light1);
        scene.add(light2);
    }

    function animate() {
        requestAnimationFrame(animate);
        controls.update();
        animateParticles();
        updateLines();
        render();
    }

    function render() {
        renderer.render(scene, camera);
    }

    function onWindowResized(event) {
        width = window.innerWidth;
        height = window.innerHeight;
        camera.aspect = width / height;
        camera.updateProjectionMatrix();
        renderer.setSize(width, height);
    }

    function createParticles() {
        for (var i = 0; i < particleCount; i++) {
            var pX = Math.random() * 50 - 25,
                pY = Math.random() * 50 - 25,
                pZ = Math.random() * 50 - 25,
                particle = new THREE.Vector3(pX, pY, pZ);
            particle.diff = Math.random() + 0.2;
            particle.default = new THREE.Vector3(pX, pY, pZ);
            particle.offset = new THREE.Vector3(0, 0, 0);
            particle.velocity = {};
            particle.velocity.y = particle.diff * 0.5;
            particle.nodes = [];
            particles.vertices.push(particle);
        }
        particleSystem = new THREE.Points(particles, pMaterial);
        particleSystem.position.y = 0;
        scene.add(particleSystem);
    }

    function animateParticles() {
        var pCount = particleCount;
        while (pCount--) {
            var particle = particles.vertices[pCount];
            var move = Math.sin(clock.getElapsedTime() * (1 * particle.diff)) / 4;

            particle.offset.y += move * particle.velocity.y;
            particle.y = particle.default.y + particle.offset.y;

            detectCloseByPoints(particle);
        }
        particles.verticesNeedUpdate = true;
        particleSystem.rotation.y += 0.01;
        lineGroup.rotation.y += 0.01;
    }

    function updateLines() {
        for (var _lineKey in lines) {
            if (!lines.hasOwnProperty(_lineKey)) {
                continue;
            }
            lines[_lineKey].geometry.verticesNeedUpdate = true;
        }
    }

    function detectCloseByPoints(p) {
        var _pCount = particleCount;
        while (_pCount--) {
            var _particle = particles.vertices[_pCount];
            if (p !== _particle) {

                var _distance = p.distanceTo(_particle);
                var _connection = checkConnection(p, _particle);

                if (_distance < maxDistance) {
                    if (!_connection) {
                        createLine(p, _particle);
                    }
                } else if (_connection) {
                    removeLine(_connection);
                }
            }
        }
    }

    function checkConnection(p1, p2) {
        var _childNode, _parentNode;
        _childNode = p1.nodes[particles.vertices.indexOf(p2)] || p2.nodes[particles.vertices.indexOf(p1)];
        if (_childNode && _childNode !== undefined) {
            _parentNode = (_childNode == p1) ? p2 : p1;
        }
        if (_parentNode && _parentNode !== undefined) {
            return {
                parent: _parentNode,
                child: _childNode,
                lineId: particles.vertices.indexOf(_parentNode) + '-' + particles.vertices.indexOf(_childNode)
            };
        } else {
            return false;
        }
    }

    function removeLine(_connection) {
        // Could animate line out
        var childIndex = particles.vertices.indexOf(_connection.child);
        _connection.parent.nodes.splice(childIndex, 1);
        deleteLine(_connection.lineId);
    }

    function deleteLine(_id) {
        lineGroup.remove(lines[_id]);
        delete lines[_id];
    }

    function addLine(points) {
        var points = points || [new THREE.Vector3(Math.random() * 10, Math.random() * 10, Math.random() * 10), new THREE.Vector3(0, 0, 0)];
        var _lineId = particles.vertices.indexOf(points[0]) + '-' + particles.vertices.indexOf(points[1]);
        var lineGeom = new THREE.Geometry();
        if (!lines[_lineId]) {
            lineGeom.dynamic = true;
            lineGeom.vertices.push(points[0]);
            lineGeom.vertices.push(points[1]);
            var curLine = new THREE.Line(lineGeom, lineMaterial);
            curLine.touched = false;
            lines[_lineId] = curLine;
            lineGroup.add(curLine);
            return curLine;
        } else {
            return false;
        }
    }

    function createLine(p1, p2) {
        p1.nodes[particles.vertices.indexOf(p2)] = p2;
        addLine([p1, p2]);
    }

    $(document).ready(function() {
        init();
    });

I am really close, but I am not sure if its optimized. There seem to be flickering lines, and sometimes a line just stays stuck in place.

So here are my thoughts. I clicked that all I have to do is make the Vector3 points of the lines equal to the relevant particle Vector3 points. I just need to update each lines geometry.verticesNeedUpdate = true;

Also, how I manage the lines is I create a unique ID using the indexes of the 2 points, e.g. lines['8-2'] = line

Community
  • 1
  • 1
Awin
  • 261
  • 1
  • 9
  • Just a suggestion: As you're connecting nodes, keep a list for each nodes of all the other nodes to which it is connected. Then, then you reach the other side of the problem (the second connection), check if the one you're trying to connect _to_ is already connected to the one you're connecting _from_. If that doesn't make sense, let me know, and I'll elaborate. – TheJim01 Apr 29 '17 at 06:48
  • Thanks @TheJim01, could you elaborate more. I think what you are trying to explain is close to what I need. I just need to understand the logic of managing the nodes and lines. I have considered separate loops. Heres my challenge, how do I keep track of the lines that connect 2 points, an efficiently. My thoughts are that as I loop through the nodes, I check which nodes are close by, then if there is a node close enough, then I add/update a line which is assigned to both nodes. I am not sure if I am on the right track. – Awin Apr 29 '17 at 13:30
  • Yes, you're heading in the right direction. But even my suggestion was a little overkill. See my answer below for a more succinct way of checking your points. – TheJim01 Apr 29 '17 at 21:10

1 Answers1

1

The problem you're actually trying to solve is that while looping through your list of points, you're doubling the number of successful matches.

Example:

Consider a list of points, [A, B, C, D]. Your looping tests each point against all other points. For this example, A and C are the only points close enough to be considered nearby.

During the first iteration, A vs. all, you find that A and C are nearby, so you add a line. But when you're doing your iteration for C, you also find that A is nearby. This causes the second line, which you want to avoid.

Fixing it:

The solution is simple: Don't re-visit nodes you already checked. This works because the answer of distance from A to C is no different from distance from C to A.

The best way to do this is adjust your indexing for your check loop:

// (Note: This is example code, and won't "just work.")
for(var check = 0, checkLength = nodes.length; check < checkLength; ++check){
    for(var against = check + 1, against < checkLength; ++against){
        if(nodes[check].distanceTo(nodes[against]) < delta){
            buildThatLine(nodes[check], nodes[against]);
        }
    }
}

In the inner loop, the indexing is set to:

  1. Skip the current node
  2. Skip all nodes before the current node.

This is done by initializing the inner indexing to the outer index + 1.

Caveat:

This particular logic assumes that you discard all your lines for every frame. It's not the most efficient way to achieve the effect, but I'll leave making it more efficient as an exercise for you.

TheJim01
  • 8,411
  • 1
  • 30
  • 54
  • Thanks for this feedback @TheJim01. I think I have tried this approach, but the problem is that then each particle can only have a single connection and not multiple, I assume. In addition, I would like the control to animate a line in once a connection is made, so I would need more control of each line itself. Just to add to this, I think what I would like is the ability to set a limit of how many lines each point can have, and each line to be controlled and animated. – Awin Apr 30 '17 at 08:10
  • an idea just came to mind. Maybe I can have it that the lines are cleared for each frame and have the line attributes stored on the "parent" node? So for whichever point draws the line first, would store the styling and attributes of the line that is to be draw. What are your thoughts on this? – Awin Apr 30 '17 at 08:14
  • 1) It should connect each node to as many nodes as fit the criteria. Drawing a line doesn't skip the rest of the nodes. :) 2) Sure, that sounds fine, too. Just keep in mind that adding them is half the battle, and you'll also need to remove them when the points move too far apart. :) – TheJim01 Apr 30 '17 at 21:31
  • so I decided to setup a jsfiddle of what I have done so far. I am really close, but I am not sure if its optimized. There seem to be flickering lines, and sometimes a line just stays stuck in place: https://jsfiddle.net/awinhayden/387c3xa7/2/ So here are my thoughts. I clicked that all I have to do is make the Vector3 points of the lines equal to the relevant particle Vector3 points. I just need to update each lines geometry.verticesNeedUpdate = true; Oh, how I manage the lines is I create a unique ID using the indexes of the 2 points, e.g. lines['8-2'] = line; – Awin May 01 '17 at 17:23
  • Should I post a new question for this? – Awin May 01 '17 at 17:24
  • Just edit your question with an update, and remember to include your code. If your fiddle ever goes away, so does the context of what your tried to do. – TheJim01 May 01 '17 at 17:27