Detailed process of drawing three-dimensional arrow lines using three.js

Detailed process of drawing three-dimensional arrow lines using three.js

Demand: This demand is an urgent need! In a subway scene, an escape route must be shown with an arrow indicating the route. It took me no less than a dozen hours to draw this arrow, and I finally finished it, but there was still a problem. My requirement for this arrow is that, no matter whether the scene is zoomed in or out, the arrow cannot be too big or too small to be clearly seen, and the shape cannot change, otherwise it will not look like an arrow.

Uses Line2.js from three.js and an open source library MeshLine.js

Part of the code:

DrawPath.js:

/**
 * Draw a route */

import * as THREE from '../build/three.module.js';
import { MeshLine, MeshLineMaterial, MeshLineRaycast } from '../js.my/MeshLine.js';

import { Line2 } from '../js/lines/Line2.js';
import { LineMaterial } from '../js/lines/LineMaterial.js';
import { LineGeometry } from '../js/lines/LineGeometry.js';
import { GeometryUtils } from '../js/utils/GeometryUtils.js';

import { CanvasDraw } from '../js.my/CanvasDraw.js';

import { Utils } from '../js.my/Utils.js';
import { Msg } from '../js.my/Msg.js';

let DrawPath = function () {

    let _self = this;

    let _canvasDraw = new CanvasDraw();
    let utils = new Utils();
    let msg = new Msg();

    this._isDrawing = false;
    this._path = [];
    this._lines = [];
    this._arrows = [];

    let _depthTest = true;
    let _side = 0;

    let viewerContainerId = '#cc';
    let viewerContainer = $(viewerContainerId)[0];

    let objects;
    let camera;
    let turn;
    let scene;

    this.config = function (objects_, camera_, scene_, turn_) {
        objects = objects_;
        camera = camera_;
        turn = turn_;
        scene = scene_;

        this._oldDistance = 1;
        this._oldCameraPos = { x: camera.position.x, y: camera.position.y, z: camera.position.z }
    }

    this.start = function () {
        if (!this._isDrawing) {
            this._isDrawing = true;
            viewerContainer.addEventListener('click', ray);
            viewerContainer.addEventListener('mousedown', mousedown);
            viewerContainer.addEventListener('mouseup', mouseup);
        }
        msg.show("Please click on the ground to draw the line");
    }

    this.stop = function () {
        if (this._isDrawing) {
            this._isDrawing = false;
            viewerContainer.removeEventListener('click', ray);
            viewerContainer.addEventListener('mousedown', mousedown);
            viewerContainer.addEventListener('mouseup', mouseup);
        }
        msg.show("Stop drawing line");
    }

    function mousedown(params) {
        this._mousedownPosition = { x: camera.position.x, y: camera.position.y, z: camera.position.z }
    }

    function mouseup(params) {
        this._mouseupPosition = { x: camera.position.x, y: camera.position.y, z: camera.position.z }
    }

    function ray(e) {
        turn.unFocusButton();

        let raycaster = createRaycaster(e.clientX, e.clientY);
        let intersects = raycaster.intersectObjects(objects.all);
        if (intersects.length > 0) {
            let point = intersects[0].point;

            let distance = utils.distance(this._mousedownPosition.x, this._mousedownPosition.y, this._mousedownPosition.z, this._mouseupPosition.x, this._mouseupPosition.y, this._mouseupPosition.z);

            if (distance < 5) {
                _self._path.push({ x: point.x, y: point.y + 50, z: point.z });

                if (_self._path.length > 1) {
                    let point1 = _self._path[_self._path.length - 2];
                    let point2 = _self._path[_self._path.length - 1];

                    drawLine(point1, point2);
                    drawArrow(point1, point2);
                }
            }
        }
    }

    function createRaycaster(clientX, clientY) {
        let x = (clientX / $(viewerContainerId).width()) * 2 - 1;
        let y = -(clientY / $(viewerContainerId).height()) * 2 + 1;

        let standardVector = new THREE.Vector3(x, y, 0.5);

        let worldVector = standardVector.unproject(camera);

        let ray = worldVector.sub(camera.position).normalize();

        let raycaster = new THREE.Raycaster(camera.position, ray);

        return raycaster;
    }

    this.refresh = function () {
        if (_self._path.length > 1) {
            let distance = utils.distance(this._oldCameraPos.x, this._oldCameraPos.y, this._oldCameraPos.z, camera.position.x, camera.position.y, camera.position.z);
            let ratio = 1;
            if (this._oldDistance != 0) {
                ratio = Math.abs((this._oldDistance - distance) / this._oldDistance)
            }

            if (distance > 5 && ratio > 0.1) {
                console.log("======== DrawPath refresh=====================================================")
                for (let i = 0; i < _self._path.length - 1; i++) {
                    let arrow = _self._arrows[i];
                    let point1 = _self._path[i];
                    let point2 = _self._path[i + 1];
                    refreshArrow(point1, point2, arrow);
                }
                this._oldDistance = distance;
                this._oldCameraPos = { x: camera.position.x, y: camera.position.y, z: camera.position.z }
            }
        }
    }

    function drawLine(point1, point2) {
        const positions = [];

        positions.push(point1.x / 50, point1.y / 50, point1.z / 50);
        positions.push(point2.x / 50, point2.y / 50, point2.z / 50);

        let geometry = new LineGeometry();
        geometry.setPositions(positions);

        let matLine = new LineMaterial({
            color: 0x009900,
            linewidth: 0.003, // in world units with size attenuation, pixels otherwise
            dashed: true,
            depthTest: _depthTest,
            side: _side
        });

        let line = new Line2(geometry, matLine);
        line.computeLineDistances();
        line.scale.set(50, 50, 50);

        scene.add(line);
        _self._lines.push(line);

    }

    function drawArrow(point1, point2) {
        let arrowLine = _self.createArrowLine(point1, point2);
        var meshLine = arrowLine.meshLine;

        let canvasTexture = _canvasDraw.drawArrow(THREE, renderer, 300, 100); //arrow var material = new MeshLineMaterial({
            useMap: true,
            map: canvasTexture,
            color: new THREE.Color(0x00f300),
            opacity: 1,
            resolution: new THREE.Vector2($(viewerContainerId).width(), $(viewerContainerId).height()),
            lineWidth: arrowLine.lineWidth,
            depthTest: _depthTest,
            side: _side,
            repeat: new THREE.Vector2(1, 1),
            transparent: true,
            sizeAttenuation: 1
        });

        var mesh = new THREE.Mesh(meshLine.geometry, material);
        mesh.scale.set(50, 50, 50);
        scene.add(mesh);
        _self._arrows.push(mesh);

    }

    function refreshArrow(point1, point2, arrow) {
        let arrowLine = _self.createArrowLine(point1, point2);
        var meshLine = arrowLine.meshLine;

        let canvasTexture = _canvasDraw.drawArrow(THREE, renderer, 300, 100); //arrow var material = new MeshLineMaterial({
            useMap: true,
            map: canvasTexture,
            color: new THREE.Color(0x00f300),
            opacity: 1,
            resolution: new THREE.Vector2($(viewerContainerId).width(), $(viewerContainerId).height()),
            lineWidth: arrowLine.lineWidth,
            depthTest: _depthTest,
            side: _side,
            repeat: new THREE.Vector2(1, 1),
            transparent: true,
            sizeAttenuation: 1
        });

        arrow.geometry = meshLine.geometry;
        arrow.material = material;

    }

    this.createArrowLine = function (point1, point2) {
        let centerPoint = { x: (point1.x + point2.x) / 2, y: (point1.y + point2.y) / 2, z: (point1.z + point2.z) / 2 };
        let distance = utils.distance(point1.x, point1.y, point1.z, point2.x, point2.y, point2.z);

        var startPos = { x: (point1.x + point2.x) / 2 / 50, y: (point1.y + point2.y) / 2 / 50, z: (point1.z + point2.z) / 2 / 50 }

        let d = utils.distance(centerPoint.x, centerPoint.y, centerPoint.z, camera.position.x, camera.position.y, camera.position.z);
        if (d < 2000) d = 2000;
        if (d > 10000) d = 10000;
        let lineWidth = 100 * d / 4000;
        //console.log("d=", d);

        let sc = 0.035;
        var endPos = { x: startPos.x + (point2.x - point1.x) * sc * d / distance / 50, y: startPos.y + (point2.y - point1.y) * sc * d / distance / 50, z: startPos.z + (point2.z - point1.z) * sc * d / distance / 50 }

        var arrowLinePoints = [];
        arrowLinePoints.push(startPos.x, startPos.y, startPos.z);
        arrowLinePoints.push(endPos.x, endPos.y, endPos.z);

        var meshLine = new MeshLine();
        meshLine.setGeometry(arrowLinePoints);

        return { meshLine: meshLine, lineWidth: lineWidth };
    }

    this.setDepthTest = function (bl) {
        if (bl) {
            _depthTest = true;
            this._lines.map(line => {
                line.material.depthTest = true;
                line.material.side = 0;
            });
            this._arrows.map(arrow => {
                arrow.material.depthTest = true;
                arrow.material.side = 0;
            });
        } else {
            _depthTest = false;
            this._lines.map(line => {
                line.material.depthTest = false;
                line.material.side = THREE.DoubleSide;
            });
            this._arrows.map(arrow => {
                arrow.material.depthTest = false;
                arrow.material.side = THREE.DoubleSide;
            });
        }
    }

    /**
     * Cancel */
    this.undo = function () {
        scene.remove(this._lines[this._lines.length - 1]);
        scene.remove(this._arrows[this._arrows.length - 1]);
        _self._path.splice(this._path.length - 1, 1);
        _self._lines.splice(this._lines.length - 1, 1);
        _self._arrows.splice(this._arrows.length - 1, 1);
    }

}

DrawPath.prototype.constructor = DrawPath;

export { DrawPath }

Part of the code in show.js:

let drawPath;

    //Draw the line drawPath = new DrawPath();
    drawPath.config(
        objects,
        camera,
        scene,
        turn
    );

    $("#rightContainer").show();
    $("#line-start").on("click", function (event) {
        drawPath.start();
    });
    $("#line-stop").on("click", function (event) {
        drawPath.stop();
    });
    $("#line-undo").on("click", function (event) {
        drawPath.undo();
    });
    $("#line-show").on("click", function (event) {
        drawPath.refresh();
    });
    let depthTest = true;
    $("#line-depthTest").on("click", function (event) {
        if (depthTest) {
            drawPath.setDepthTest(false);
            depthTest = false;
        } else {
            drawPath.setDepthTest(true);
            depthTest = true;
        }
    });

setInterval(() => {
    drawPath && drawPath.refresh();
}, 100);

Effect picture:

There are still some problems:

Although the scene is zoomed in and the arrow is a bit large in this rendering, the maximum size is still controlled. It’s just that the shape has a problem, which may be a problem of perspective.

The effect I expect should be like this, that is, no matter from what angle you look at it, the arrow should not be deformed:

This is the end of this article about using three.js to draw three-dimensional arrow lines that took half of my life. For more related three.js three-dimensional arrow lines content, please search 123WORDPRESS.COM's previous articles or continue to browse the following related articles. I hope everyone will support 123WORDPRESS.COM in the future!

You may also be interested in:
  • Three.js realizes Facebook Metaverse 3D dynamic logo effect
  • Use three.js to achieve cool acid style 3D page effects
  • How to achieve 3D dynamic text effect with three.js
  • Three.js sample code for implementing dewdrop animation effect
  • Detailed explanation of the use and performance testing of multithreading in three.js
  • First experience of creating text with javascript Three.js

<<:  Learn how to write neat and standard HTML tags

>>:  A brief discussion of the interesting box model of CSS3 box-sizing property

Recommend

The front end creates and modifies CAD graphics details through JavaScript

Table of contents 1. Current situation 2. Create ...

Tutorial on installing mysql under centos7

Recently, I plan to deploy a cloud disk on my hom...

Analyze the method of prometheus+grafana monitoring nginx

Table of contents 1. Download 2. Install nginx an...

How to transfer files between Windows and Linux

File transfer between Windows and Linux (1) Use W...

Detailed explanation of the functions and usage of MySQL common storage engines

This article uses examples to illustrate the func...

Ubuntu 20.04 connects to wifi (2 methods)

I recently installed Ubuntu 20.04 and found that ...

Analysis of the principles and usage of Docker container data volumes

What is a container data volume If the data is in...

Detailed introduction to linux host name configuration

Table of contents 1. Configure Linux hostname Con...

HTML is something that web page creators must learn and master.

What are the benefits of learning HTML? 1: Easily...

nginx automatically generates configuration files in docker container

When a company builds Docker automated deployment...