用 three.js 绘制三维带箭头线 (线内箭头)

在LineMaterial.js基础上修改的ArrowLineMaterial.js代码:

/**
 * @author WestLangley / http://github.com/WestLangley
 *
 * parameters = {
 *  color: <hex>,
 *  line <float>,
 *  dashed: <boolean>,
 *  dashScale: <float>,
 *  dashSize: <float>,
 *  gapSize: <float>,
 *  resolution: <Vector2>, // to be set by renderer
 * }
 */

import {
    ShaderLib,
    ShaderMaterial,
    UniformsLib,
    UniformsUtils,
    Vector2
} from "../build/three.module.js";

UniformsLib.line = {

    line { value: 1 },
    resolution: { value: new Vector2(1, 1) },
    dashScale: { value: 1 },
    dashSize: { value: 1 },
    gapSize: { value: 1 } // todo FIX - maybe change to totalSize

};

ShaderLib['line'] = {

    uniforms: UniformsUtils.merge([
        UniformsLib.common,
        UniformsLib.fog,
        UniformsLib.line
    ]),

    vertexShader:
        `
        #include <common>
        #include <color_pars_vertex>
        #include <fog_pars_vertex>
        #include <logdepthbuf_pars_vertex>
        #include <clipping_planes_pars_vertex>

        uniform float linewidth;
        uniform vec2 resolution;

        attribute vec3 instanceStart;
        attribute vec3 instanceEnd;

        attribute vec3 instanceColorStart;
        attribute vec3 instanceColorEnd;

        varying vec2 vUv;

        varying float lineLength;

        #ifdef USE_DASH

            uniform float dashScale;
            attribute float instanceDistanceStart;
            attribute float instanceDistanceEnd;
            varying float vLineDistance;

        #endif

        void trimSegment( const in vec4 start, inout vec4 end ) {

            // trim end segment so it terminates between the camera plane and the near plane

            // conservative estimate of the near plane
            float a = projectionMatrix[ 2 ][ 2 ]; // 3nd entry in 3th column
            float b = projectionMatrix[ 3 ][ 2 ]; // 3nd entry in 4th column
            float nearEstimate = - 0.5 * b / a;

            float alpha = ( nearEstimate - start.z ) / ( end.z - start.z );

            end.xyz = mix( start.xyz, end.xyz, alpha );

        }

        void main() {

            #ifdef USE_COLOR

                vColor.xyz = ( position.y < 0.5 ) ? instanceColorStart : instanceColorEnd;

            #endif

            #ifdef USE_DASH

                vLineDistance = ( position.y < 0.5 ) ? dashScale * instanceDistanceStart : dashScale * instanceDistanceEnd;

            #endif

            float aspect = resolution.x / resolution.y;

            vUv = uv;

            // camera space
            vec4 start = modelViewMatrix * vec4( instanceStart, 1.0 );
            vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 );

            // special case for perspective projection, and segments that terminate either in, or behind, the camera plane
            // clearly the gpu firmware has a way of addressing this issue when projecting into ndc space
            // but we need to perform ndc-space calculations in the shader, so we must address this issue directly
            // perhaps there is a more elegant solution -- WestLangley

            bool perspective = ( projectionMatrix[ 2 ][ 3 ] == - 1.0 ); // 4th entry in the 3rd column

            if ( perspective ) {

                if ( start.z < 0.0 && end.z >= 0.0 ) {

                    trimSegment( start, end );

                } else if ( end.z < 0.0 && start.z >= 0.0 ) {

                    trimSegment( end, start );

                }

            }

            // clip space
            vec4 clipStart = projectionMatrix * start;
            vec4 clipEnd = projectionMatrix * end;

            // ndc space
            vec2 ndcStart = clipStart.xy / clipStart.w;
            vec2 ndcEnd = clipEnd.xy / clipEnd.w;

            // direction
            vec2 dir = ndcEnd - ndcStart;

            // account for clip-space aspect ratio
            dir.x *= aspect;
            dir = normalize( dir );

            // perpendicular to dir
            vec2 offset = vec2( dir.y, - dir.x );

            // undo aspect ratio adjustment
            dir.x /= aspect;
            offset.x /= aspect;

            // sign flip
            if ( position.x < 0.0 ) offset *= - 1.0;

            // endcaps
            if ( position.y < 0.0 ) {

                offset += - dir;

            } else if ( position.y > 1.0 ) {

                offset += dir;

            }

            // adjust for linewidth
            offset *= linewidth;

            // adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...
            offset /= resolution.y;

            // select end
            vec4 clip = ( position.y < 0.5 ) ? clipStart : clipEnd;

            // back to clip space
            offset *= clip.w;

            clip.xy += offset;

            gl_Position = clip;

            vec4 mvPosition = ( position.y < 0.5 ) ? start : end; // this is an approximation

            //lineLength = distance(ndcStart, ndcEnd);
            lineLength = distance(ndcStart, ndcEnd) * (1.57 + abs(atan(dir.x / dir.y))) / 2.0;
            //lineLength = distance(clipStart.xyz, clipEnd.xyz);
            //lineLength = distance(start.xyz, end.xyz);

            #include <logdepthbuf_vertex>
            #include <clipping_planes_vertex>
            #include <fog_vertex>

        }
        `,

    fragmentShader:
        `
        uniform vec3 diffuse;
        uniform float opacity;
        uniform sampler2D map;

        varying float lineLength;

        #ifdef USE_DASH

            uniform float dashSize;
            uniform float gapSize;

        #endif

        varying float vLineDistance;

        #include <common>
        #include <color_pars_fragment>
        #include <fog_pars_fragment>
        #include <logdepthbuf_pars_fragment>
        #include <clipping_planes_pars_fragment>

        varying vec2 vUv;

        void main() {

            #include <clipping_planes_fragment>

            #ifdef USE_DASH

                if ( vUv.y < - 1.0 || vUv.y > 1.0 ) discard; // discard endcaps

                if ( mod( vLineDistance, dashSize + gapSize ) > dashSize ) discard; // todo - FIX

            #endif

            if ( abs( vUv.y ) > 1.0 ) {

                float a = vUv.x;
                float b = ( vUv.y > 0.0 ) ? vUv.y - 1.0 : vUv.y + 1.0;
                float len2 = a * a + b * b;

                if ( len2 > 1.0 ) discard;
            }

            vec4 diffuseColor = vec4( diffuse, opacity );

            #include <logdepthbuf_fragment>
            #include <color_fragment>

            vec4 c;

            if ( abs( vUv.y ) > 1.0 ) {
                c = vec4(diffuseColor.rgb, diffuseColor.a); 
            } else {
                vec2 rpt = vec2(0.5, 1.0);
                
                rpt.y *= lineLength * 5.0;
                //rpt.y *= lineLength / 500.0;

                rpt.y = floor(rpt.y + 0.5);
                if(rpt.y < 1.0) { rpt.y = 1.0; }
                if(rpt.y > 5.0) { rpt.y = 5.0; }
                c = vec4(1.0, 1.0, 1.0, 1.0); 
                c *= texture2D( map, vUv * rpt );
            }
            
            gl_FragColor = c;

            //#include <premultiplied_alpha_fragment>
            //#include <tonemapping_fragment>
            //#include <encodings_fragment>
            //#include <fog_fragment>

        }
        `
};

var ArrowLineMaterial = function (parameters) {

    ShaderMaterial.call(this, {

        type: 'ArrowLineMaterial',

        uniforms: Object.assign({}, UniformsUtils.clone(ShaderLib['line'].uniforms), {
            map: { value: null },
        }),

        vertexShader: ShaderLib['line'].vertexShader,
        fragmentShader: ShaderLib['line'].fragmentShader,

        clipping: true // required for clipping support

    });

    this.dashed = false;

    Object.defineProperties(this, {

        map: {

            enumerable: true,

            get: function () {

                return this.uniforms.map.value;

            },

            set: function (value) {

                this.uniforms.map.value = value;

            }

        },

        color: {

            enumerable: true,

            get: function () {

                return this.uniforms.diffuse.value;

            },

            set: function (value) {

                this.uniforms.diffuse.value = value;

            }

        },

        line {

            enumerable: true,

            get: function () {

                return this.uniforms.linewidth.value;

            },

            set: function (value) {

                this.uniforms.linewidth.value = value;

            }

        },

        dashScale: {

            enumerable: true,

            get: function () {

                return this.uniforms.dashScale.value;

            },

            set: function (value) {

                this.uniforms.dashScale.value = value;

            }

        },

        dashSize: {

            enumerable: true,

            get: function () {

                return this.uniforms.dashSize.value;

            },

            set: function (value) {

                this.uniforms.dashSize.value = value;

            }

        },

        gapSize: {

            enumerable: true,

            get: function () {

                return this.uniforms.gapSize.value;

            },

            set: function (value) {

                this.uniforms.gapSize.value = value;

            }

        },

        resolution: {

            enumerable: true,

            get: function () {

                return this.uniforms.resolution.value;

            },

            set: function (value) {

                this.uniforms.resolution.value.copy(value);

            }

        }

    });

    this.setValues(parameters);

};

ArrowLineMaterial.prototype = Object.create(ShaderMaterial.prototype);
ArrowLineMaterial.prototype.constructor = ArrowLineMaterial;

ArrowLineMaterial.prototype.isLineMaterial = true;


export { ArrowLineMaterial };
View Code

ArrowLineMaterial.js中主要修改部分:

在顶点着色器中定义变量:

varying float lineLength;
View Code

在顶点着色器中计算一下线的长度:

lineLength = distance(ndcStart, ndcEnd) * (1.57 + abs(atan(dir.x / dir.y))) / 2.0;
View Code

在片元着色器中定义变量:

uniform sampler2D map;
varying float lineLength;
View Code

在片元着色器中贴图:

vec4 c;

if ( abs( vUv.y ) > 1.0 ) {
    c = vec4(diffuseColor.rgb, diffuseColor.a); 
} else {
    vec2 rpt = vec2(0.5, 1.0);
    
    rpt.y *= lineLength * 5.0;
    //rpt.y *= lineLength / 500.0;

    rpt.y = floor(rpt.y + 0.5);
    if(rpt.y < 1.0) { rpt.y = 1.0; }
    if(rpt.y > 5.0) { rpt.y = 5.0; }
    c = vec4(1.0, 1.0, 1.0, 1.0); 
    c *= texture2D( map, vUv * rpt );
}

gl_FragColor = c;
View Code

在片元着色器中注释掉下面几行,使线的颜色和canvas中设置的颜色一致:

//#include <premultiplied_alpha_fragment>
//#include <tonemapping_fragment>
//#include <encodings_fragment>
//#include <fog_fragment>
View Code

CanvasDraw.js代码:

/**
 * canvas绘图
 */

let CanvasDraw = function () {

    /**
     * 画文本和气泡
     */
    this.drawText = function (THREE, renderer, text, width) {
        let canvas = document.createElement("canvas");
        let ctx = canvas.getContext('2d');

        canvas.width = width * 2;
        canvas.height = width * 2;

        this.drawBubble(ctx, width - 10, width - 65, width, 45, 6, "#00c864");

        //设置文字
        ctx.fillStyle = "#ffffff";
        ctx.font = '32px 宋体';
        ctx.fillText(text, width - 10 + 12, width - 65 + 34);

        let canvasTexture = new THREE.CanvasTexture(canvas);
        canvasTexture.magFilter = THREE.NearestFilter;
        canvasTexture.minFilter = THREE.NearestFilter;

        let maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
        canvasTexture.anisotropy = maxAnisotropy;

        return canvasTexture;
    }

    /**
     * 画箭头
     */
    this.drawArrow = function (THREE, renderer, width, height) {
        let canvas = document.createElement("canvas");
        let ctx = canvas.getContext('2d');

        canvas.width = width;
        canvas.height = height;

        ctx.save();

        ctx.translate(0, 0);

        //this.drawRoundRectPath(ctx, width, height, 0);

        //ctx.fillStyle = "#ffff00";
        //ctx.fill();

        this.drawArrowBorder(ctx, 2, 0, 0, 4, 100, 50, 0, 96, 2, 100, 300, 50);
        ctx.fillStyle = "#ffffff";
        ctx.fill();

        ctx.restore();

        let canvasTexture = new THREE.CanvasTexture(canvas);
        canvasTexture.magFilter = THREE.NearestFilter;
        canvasTexture.minFilter = THREE.NearestFilter;

        let maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
        canvasTexture.anisotropy = maxAnisotropy;

        return canvasTexture;
    }

    /**
     * 画线内箭头
     */
    this.drawArrow3 = function (THREE, renderer, width, height, color) {
        let canvas = document.createElement("canvas");
        let ctx = canvas.getContext('2d');

        canvas.width = width;
        canvas.height = height;

        ctx.save();

        ctx.translate(0, 0);

        this.drawRoundRectPath(ctx, width, height, 0);

        ctx.fillStyle = color;
        ctx.fill();

        this.drawArrowBorder(ctx, 0, 350, 0, 400, 50, 450, 100, 400, 100, 350, 50, 400);
        ctx.fillStyle = "#ffffff";
        ctx.fill();

        ctx.restore();

        let canvasTexture = new THREE.CanvasTexture(canvas);
        canvasTexture.magFilter = THREE.NearestFilter;
        canvasTexture.minFilter = THREE.NearestFilter;
        canvasTexture.wrapS = THREE.RepeatWrapping;
        canvasTexture.wrapT = THREE.RepeatWrapping;

        let maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
        canvasTexture.anisotropy = maxAnisotropy;

        return canvasTexture;
    }

    /**
     * 画气泡
     */
    this.drawBubble = function (ctx, x, y, width, height, radius, fillColor) {
        ctx.save();

        ctx.translate(x, y);

        this.drawRoundRectPath(ctx, width, height, radius);

        ctx.fillStyle = fillColor || "#000";
        ctx.fill();

        this.drawTriangle(ctx, 20, height, 40, height, 10, 65);
        ctx.fillStyle = fillColor || "#000";
        ctx.fill();

        ctx.restore();
    }

    /**
     * 画三角形
     */
    this.drawTriangle = function (ctx, x1, y1, x2, y2, x3, y3) {
        ctx.beginPath();

        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.lineTo(x3, y3);

        ctx.closePath();
    }

    /**
     * 画箭头边框
     */
    this.drawArrowBorder = function (ctx, x1, y1, x2, y2, x3, y3, x4, y4, x5, y5, x6, y6) {
        ctx.beginPath();

        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.lineTo(x3, y3);
        ctx.lineTo(x4, y4);
        ctx.lineTo(x5, y5);
        ctx.lineTo(x6, y6);

        ctx.closePath();
    }

    /**
     * 画圆角矩形
     */
    this.drawRoundRectPath = function (ctx, width, height, radius) {
        ctx.beginPath(0);

        //从右下角顺时针绘制,弧度从0到1/2PI  
        ctx.arc(width - radius, height - radius, radius, 0, Math.PI / 2);

        //矩形下边线  
        ctx.lineTo(radius, height);

        //左下角圆弧,弧度从1/2PI到PI  
        ctx.arc(radius, height - radius, radius, Math.PI / 2, Math.PI);

        //矩形左边线  
        ctx.lineTo(0, radius);

        //左上角圆弧,弧度从PI到3/2PI  
        ctx.arc(radius, radius, radius, Math.PI, Math.PI * 3 / 2);

        //上边线  
        ctx.lineTo(width - radius, 0);

        //右上角圆弧  
        ctx.arc(width - radius, radius, radius, Math.PI * 3 / 2, Math.PI * 2);

        //右边线  
        ctx.lineTo(width, height - radius);

        ctx.closePath();
    }

    /**
     * 画圆
     */
    this.drawCircle = function (THREE, renderer, width, height, radius, fillColor) {
        let canvas = document.createElement("canvas");
        let ctx = canvas.getContext('2d');

        canvas.width = width;
        canvas.height = height;

        ctx.save();

        ctx.beginPath(0);

        ctx.arc(width / 2, height / 2, radius, 0, 2 * Math.PI);

        ctx.closePath();

        ctx.fillStyle = fillColor || "#000";
        ctx.fill();

        ctx.restore();

        let texture = new THREE.CanvasTexture(canvas);
        texture.needsUpdate = true;

        texture.magFilter = THREE.NearestFilter;
        texture.minFilter = THREE.NearestFilter;

        let maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
        texture.anisotropy = maxAnisotropy;

        return texture;
    }

}

CanvasDraw.prototype.constructor = CanvasDraw;

export { CanvasDraw }
View Code

DrawPath2.js代码:

/**
 * 绘制路线
 */

import * as THREE from '../build/three.module.js';

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

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

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

let DrawPath2 = function () {

    let _self = this;

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

    this._isDrawing = false;
    this._path = [];
    this._lines = [];
    this.color = '#00F300';

    this._depthTest = true;
    this._hide = false;

    let _side = 0;

    let viewerContainerId = '#threeCanvas';
    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);
        }
    }

    this.stop = function () {
        if (this._isDrawing) {
            this._isDrawing = false;
            viewerContainer.removeEventListener('click', ray);
            viewerContainer.removeEventListener('mousedown', mousedown);
            viewerContainer.removeEventListener('mouseup', mouseup);
        }
    }

    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 objs = [];
        objects.all.map(object => {
            if (object.material.visible) {
                objs.push(object);
            }
        });
        let intersects = raycaster.intersectObjects(objs);
        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);
                }
            }
        }
    }

    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 () {

    }

    function drawLine(point1, point2) {
        let n = Math.round(utils.distance(point1.x, point1.y, point1.z, point2.x, point2.y, point2.z) / 500);
        if (n < 1) n = 1;
        for (let i = 0; i < n; i++) {
            let p1 = {};
            p1.x = point1.x + (point2.x - point1.x) / n * i;
            p1.y = point1.y + (point2.y - point1.y) / n * i;
            p1.z = point1.z + (point2.z - point1.z) / n * i;

            let p2 = {};
            p2.x = point1.x + (point2.x - point1.x) / n * (i + 1);
            p2.y = point1.y + (point2.y - point1.y) / n * (i + 1);
            p2.z = point1.z + (point2.z - point1.z) / n * (i + 1);

            drawLine2(p1, p2);
        }
    }

    function drawLine2(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);

        geometry.setColors([
            parseInt(_self.color.substr(1, 2), 16) / 256,
            parseInt(_self.color.substr(3, 2), 16) / 256,
            parseInt(_self.color.substr(5, 2), 16) / 256,
            parseInt(_self.color.substr(1, 2), 16) / 256,
            parseInt(_self.color.substr(3, 2), 16) / 256,
            parseInt(_self.color.substr(5, 2), 16) / 256
        ]);

        let canvasTexture = _canvasDraw.drawArrow3(THREE, renderer, 100, 800, _self.color); //箭头

        let matLine = new ArrowLineMaterial({
            map: canvasTexture,
            color: new THREE.Color(0xffffff),
            line 0.005, // in world units with size attenuation, pixels otherwise
            dashed: false,
            depthTest: _self._depthTest,
            side: _side,
            vertexColors: THREE.VertexColors,
            resolution: new THREE.Vector2(1, $(viewerContainerId).height() / $(viewerContainerId).width())
        });

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

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

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

    this.getPath = function () {
        return this._path;
    }

    this.hide = function () {
        this._lines.map(line => scene.remove(line));
        this._hide = true;
    }

    this.show = function () {
        this._lines.map(line => scene.add(line));
        this._hide = false;
    }

    this.isShow = function () {
        return !this._hide;
    }

    this.create = function (path, color) {
        _self.color = color;
        _self._path = path;

        if (_self._path.length > 1) {
            for (let i = 0; i < _self._path.length - 1; i++) {
                let point1 = _self._path[i];
                let point2 = _self._path[i + 1];

                drawLine(point1, point2);
            }
        }
    }

    this.getDepthTest = function () {
        return _self._depthTest;
    }

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

}

DrawPath2.prototype.constructor = DrawPath2;

export { DrawPath2 }
View Code

效果图:

 

缺陷:

2.5D视角观察,看着还行,但是把相机拉近观察,箭头就会变形。凑合着用。

箭头贴图变形或者箭头显示不全,原因我猜可能是因为在场景中,线的远离相机的一端,在标准设备坐标系中比较细,线的靠近相机的一端,在标准设备坐标系中比较粗,但为了使线的粗细一样,靠近相机的一端被裁剪了,所以箭头可能会显示不全。

不管是MeshLine还是three.js的Line2,这个带宽度的线,和三维场景中的三维模型是有区别的,无论场景拉近还是拉远,线的宽度不变,而三维模型场景拉远变小,拉近变大。

Drawing arrow lines is hard!

参考文章:

https://www.cnblogs.com/dojo-lzz/p/9219290.html

https://blog.csdn.net/Amesteur/article/details/95964526

 

原文地址:https://www.cnblogs.com/s0611163/p/15549819.html