在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 };
ArrowLineMaterial.js中主要修改部分:
在顶点着色器中定义变量:
varying float lineLength;
在顶点着色器中计算一下线的长度:
lineLength = distance(ndcStart, ndcEnd) * (1.57 + abs(atan(dir.x / dir.y))) / 2.0;
在片元着色器中定义变量:
uniform sampler2D map; varying float lineLength;
在片元着色器中贴图:
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;
在片元着色器中注释掉下面几行,使线的颜色和canvas中设置的颜色一致:
//#include <premultiplied_alpha_fragment> //#include <tonemapping_fragment> //#include <encodings_fragment> //#include <fog_fragment>
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 }
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 }
效果图:
缺陷:
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