G  A
G A

Reputation: 63

Draw a 2D line with width in three.js

I'm looking to draw a continuous line with a given thickness showing only the edges using three.js. I have achieved it. I'm trying to add thickness to the line but it is not getting reflected in the scene due to some angle in three.js. Can anyone help me out with the issue.enter image description here Here's the fiddle https://jsfiddle.net/16vhjm0y/1/

        var renderer, scene, camera;
    var line;
    var count = 0;
    var mouse = new THREE.Vector3();
    var mesh3D;
    var maxPoint = 6;
    var height = window.innerHeight * .99;

    var plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0); // facing us for mouse intersection
    var raycaster = new THREE.Raycaster();

    var point3ds = [];

    var usePerspectiveCamera = false;   // toggles back and forth

    var perspOrbit;
    var perspCam;

    var orthoOrbit;
    var orthoCam;
    var labelRenderer, labelAjay;
    var testBoolean = false;
    var mouseDownBoolean = false;
    var distanceData, showDistanceData;
    var ajay;
    var arrAjay = [];
    var arrAjayFinal = [];
    var mouseUpBoolean = false;
    init();
    animate();

    function init() {


        // renderer
        renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, height);
        document.body.appendChild(renderer.domElement);

        // scene
        scene = new THREE.Scene();
        scene.background = new THREE.Color(0xffffff);
        // camera perspective
        perspCam = new THREE.PerspectiveCamera(45, window.innerWidth / height, 1, 10000);
        perspCam.position.set(0, 0, 200);

        // camera ortho
        var width = window.innerWidth;
        //var height = window.innerHeight;
        orthoCam = new THREE.OrthographicCamera(-width / 2, width / 2, height / 2, -height / 2, 0, 1200);

        // assign cam
        camera = perspCam;

        someMaterial = new THREE.MeshBasicMaterial({ color: 0xA9A9A9, side: THREE.DoubleSide, transparent: true, opacity: 0.3 });

        // grid
        var grid = new THREE.GridHelper(1024, 56);
        grid.rotateX(Math.PI / 2);
        // scene.add(grid);

       // geometry
        var geometry = new THREE.BufferGeometry();
        var MAX_POINTS = 500;
        positions = new Float32Array(MAX_POINTS * 3);
        geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3));

        // material
        var material = new THREE.LineBasicMaterial({
            color: 0xff0000,
            linewidth: 10
        });

        // line
        line = new THREE.Line(geometry, material);
        // line.position.z = 20;
        scene.add(line);

        // var geometry = new THREE.BoxBufferGeometry( 10, 2, 20 );
        // var edgesPavement = new THREE.EdgesGeometry( geomPavement );
        // var lineGeometry = new THREE.LineSegmentsGeometry().setPositions( edgesPavement.attributes.position.array );
        // var lineMaterial = new THREE.LineMaterial( { color: 0xff0000, linewidth: 10 } );
        // lineMaterial.resolution.set( window.innerWidth, window.innerHeight ); // important, for now...
        // var line = new THREE.LineSegments2( lineGeometry, lineMaterial );
        // scene.add( line );

        document.addEventListener("mousemove", onMouseMove, false);
        document.addEventListener('mousedown', onMouseDown, false);
        document.addEventListener('mouseup', onMouseUp, false);

        createUI();

        labelRenderer = new THREE.CSS2DRenderer();

        
        ajay = document.createElement('div');
        ajay.className = 'ajay';
        ajay.style.color = "black";

        ajayInsert = document.createElement('div');
        ajayInsert.className = 'ajay';
        ajayInsert.style.color = "black";

        // ajay.style.color = "black";
        // console.log(ajay)
        labelAjay = new THREE.CSS2DObject(ajay);
        labelAjayFinal = new THREE.CSS2DObject(ajayInsert);

        labelRenderer.setSize(window.innerWidth, window.innerHeight);
        labelRenderer.domElement.style.position = 'absolute';
        labelRenderer.domElement.style.top = '0';
        labelRenderer.domElement.style.pointerEvents = 'none';
        ajay.style.display = "none";
        ajayInsert.style.display = "none";

    }

    // update line
    function updateLine() {

        positions[count * 3 - 3] = mouse.x;
        positions[count * 3 - 2] = mouse.y;
        positions[count * 3 - 1] = mouse.z;
        line.geometry.attributes.position.needsUpdate = true;
    }

    // mouse move handler
    function onMouseMove(event) {

        var rect = renderer.domElement.getBoundingClientRect();
        mouse.x = (event.clientX - rect.left) / (rect.right - rect.left) * 2 - 1;
        mouse.y = - ((event.clientY - rect.top) / (rect.bottom - rect.top)) * 2 + 1;

        raycaster.setFromCamera(mouse, camera);
        mouse = raycaster.ray.intersectPlane(plane, mouse);

        if (count !== 0 && count < maxPoint) {
            updateLine();
        }
        testBoolean = true;
        if (testBoolean == true) {
            // scene.remove(labelAjay);
            var geometry = line.geometry;
            geometry.computeBoundingBox();
            center = geometry.boundingBox.getCenter();
            // line.localToWorld(center);
            // console.log(center);
            if (mouseDownBoolean == true) {
                labelAjay.position.set(mouse.x, mouse.y, mouse.z);

                // console.log(line.position)
                scene.add(labelAjay);
                document.body.appendChild(labelRenderer.domElement);

                //   console.log(positions);
                distanceData = point3ds[0].distanceTo(new THREE.Vector3(mouse.x, mouse.y, mouse.z));
                showDistanceData = Math.round(distanceData * 1000); 
                //   console.log(point3ds[0]);
                //   console.log(point3ds[1]);
                //   console.log(distanceData);
                //   console.log(showDistanceData)
                 ajay.textContent = showDistanceData + ' mm';

                // console.log(labelRenderer)
            }

            // console.log(labelRenderer.domElement)
            // document.getElementsByClassName("ajay").remove();
            // document.getElementsByClassName("ajay").outerHTML = "";
        }
    }

    // add point
    function addPoint(event) {
        if (count < maxPoint) {
            console.log("point nr " + count + ": " + mouse.x + " " + mouse.y + " " + mouse.z);
            positions[count * 3 + 0] = mouse.x;
            positions[count * 3 + 1] = mouse.y;
            positions[count * 3 + 2] = mouse.z
            count++;

            line.geometry.setDrawRange(0, count);
            updateLine();
            point3ds.push(new THREE.Vector3(mouse.x, mouse.y, mouse.z));
        } else {
            console.log('max points reached: ' + maxPoint);
        }
    }

    function getPointInBetweenByLen(pointA, pointB, length) {

        var dir = pointB.clone().sub(pointA).normalize().multiplyScalar(length);
        return pointA.clone().add(dir);

    }
    // mouse down handler
    function onMouseDown(evt) {
        mouseDownBoolean = true;
        // force add an extra point on first click so buffer line can display
        // buffer geometry requires two points to display, so first click should add two points
        if (count === 0) {
            addPoint();
        }

        if (count < maxPoint) {
            addPoint();
        }
    }

   function onMouseUp(event){
        mouseUpBoolean = true;
       if(mouseUpBoolean == true){

    // showDistanceData = Math.round(distanceData * 1000);
    arrAjay.push(showDistanceData);
    console.log(arrAjay);
    
    arrAjayFinal = arrAjay.splice(-1)[0];
    var geometry = line.geometry;
            geometry.computeBoundingBox();
            center = geometry.boundingBox.getCenter();
           
            if (mouseDownBoolean == true) {
                labelAjayFinal.position.set(center.x, center.y, center.z);

                scene.add(labelAjayFinal);
                document.body.appendChild(labelRenderer.domElement);

                // distanceData = point3ds[0].distanceTo(new THREE.Vector3(mouse.x, mouse.y, mouse.z));
                // showDistanceData = Math.round(distanceData * 1000);
                console.log('arrAjayFinal', arrAjayFinal);

                ajayInsert.textContent = arrAjayFinal;
            }
       }
   }
    // render
    function render() {
        renderer.render(scene, camera);
        labelRenderer.render(scene, camera);
    }

    // animate
    function animate() {
        requestAnimationFrame(animate);
        render();
    }

    // loop through all the segments and create their 3D
    function create3D() {

        if (!mesh3D && point3ds && point3ds.length) {
            console.log('creating 3D');
            mesh3D = new THREE.Mesh();  // metpy mesh but is the root mesh for all 3D
            scene.add(mesh3D);
            // prepare create segments from point3ds - every two points create a segement
            var index = 1;
            var segmentHeight = 56;
            point3ds.forEach(point3d => {
                if (index < point3ds.length) {
                    var seg = new Segment(point3d, point3ds[index], someMaterial, segmentHeight);
                    mesh3D.add(seg.mesh3D);
                    index++;
                }
            });


        }
    }

    function createUI() {

        // create3D
        var btn = document.createElement('button');
        document.body.appendChild(btn);
        btn.innerHTML = 'Create3D';
        btn.addEventListener('mousedown', () => {
            create3D();

            // add orbiting controls to both cameras
            var controls;
            if (!perspOrbit) {
                perspOrbit = new THREE.OrbitControls(perspCam, renderer.domElement);
                perspOrbit.screenSpacePanning = true;
                // raotation is enabled once create3D is pressed
                setToFullOrbit(perspOrbit);
                perspOrbit.enabled = true;  // set to true by default

            }

            // add orbit to orthocam
            if (!orthoOrbit) {
                orthoOrbit = new THREE.OrbitControls(orthoCam, renderer.domElement);
                orthoOrbit.screenSpacePanning = true;
                orthoOrbit.enabled = false; // set to false by default
                //orthoOrbit.enableDamping = true;
                //orthoOrbit.dampingFactor = .15;
            }

        });

    }

    function switchCam() {
        usePerspectiveCamera = !usePerspectiveCamera;
        if (usePerspectiveCamera) {
            if (perspCam) {
                camera = perspCam;
                perspOrbit.enabled = true;
                orthoOrbit.enabled = false;
            } else {
                throw new Error('Switch to perspective cam failed, perspective cam is null');
            }
        } else {
            if (orthoCam) {
                camera = orthoCam;
                orthoOrbit.enabled = true;
                perspOrbit.enabled = false;
            } else {
                throw new Error('Switch to ortho cam failed, orthoCam is null');
            }
        }
    }

    function rotateCam90() {
        if (camera instanceof THREE.OrthographicCamera) {
            orthoOrbit.update();
            camera.applyMatrix(new THREE.Matrix4().makeRotationZ(Math.PI / 2));
        }
    }

    function reset() {
        scene.remove(mesh3D);
        mesh3D = null;
        for (var i = 0; i < 3 * 8; i++) {
            positions[i] = 0;
        }
        count = 0;
        line.geometry.setDrawRange(0, count);
        updateLine();
        point3ds = [];

    }

    function setToFullOrbit(orbitControl) {
        // how far you can orbit vertically
        orbitControl.minPolarAngle = 0;
        orbitControl.maxPolarAngle = Math.PI;

        // How far you can dolly in and out ( PerspectiveCamera only )
        orbitControl.minDistance = 0;
        orbitControl.maxDistance = Infinity;

        orbitControl.enableZoom = true; // Set to false to disable zooming
        orbitControl.zoomSpeed = 1.0;

        orbitControl.enableRotate = true;

        // allow keyboard arrows
        orbitControl.enableKeys = true;

        // Set to false to disable panning (ie vertical and horizontal translations)
        orbitControl.enablePan = true;
    }

    // each segment knows how to create its 3D
    class Segment {
        constructor(start, end, material, height) {
            this.start = start;
            this.end = end;
            this.height = height; // height of the segment's 3D
            this.material = material;
            this.mesh3D = null;
            this.create3D();
        }
        create3D() {
            if (this.start && this.end) {
                //create the shape geometry
                var distStartToEnd = this.start.distanceTo(this.end);

                var vec2s = [
                    new THREE.Vector2(),
                    new THREE.Vector2(0, this.height),
                    new THREE.Vector2(distStartToEnd, this.height),
                    new THREE.Vector2(distStartToEnd, 0)
                ];
                console.log('vec2s', vec2s);
                var shape = new THREE.Shape(vec2s);
                var geo = new THREE.BoxGeometry(5, 5, 5);
                // console.log('shape', shape);
                var geo = new THREE.ShapeGeometry(shape);
                geo.applyMatrix(new THREE.Matrix4().makeRotationX(THREE.Math.degToRad(90)));
                this.mesh3D = new THREE.Mesh(geo, this.material);
                this.alignRotation();
                this.alignPosition();
                // the mesh3D should be added to the scene outside of this class

            }
        }

        alignRotation() {
            var p1 = this.start.clone();
            var p2 = this.end.clone();
            var direction = new THREE.Vector3();
            direction.subVectors(p2, p1);
            direction.normalize();
            this.mesh3D.quaternion.setFromUnitVectors(new THREE.Vector3(1, 0, 0), direction);
        }

        alignPosition() {
            if (this.mesh3D) {
                this.mesh3D.position.copy(this.start);
            } else {
                throw new Error('mesh3D null');
            }
        }

    }

Upvotes: 1

Views: 3421

Answers (1)

M -
M -

Reputation: 28462

The linewidth parameter relies on native WebGL support for drawing line thickness, but its performance is very spotty across browsers & operating systems. I think Windows doesn't support it, but MacOS does, so it shouldn't be relied upon. See this discussion on the Three.js Github for several bug reports.

As a workaround, they've created LineGeometry, which sort of re-builds a line with geometry to allow for thickness. See this example for how to use it. It even allows for dashed lines. After importing the module, you can implement it with:

const geometry = new LineGeometry();
geometry.setPositions( positions );
geometry.setColors( colors );

matLine = new LineMaterial( {
    color: 0xffffff,
    linewidth: 5, // in pixels
    vertexColors: true,
    dashed: false
} );

line = new Line2( geometry, matLine );
line.computeLineDistances();
scene.add( line );

Upvotes: 2

Related Questions