需求:这个需求是个刚需啊!在一个地铁场景里展示逃生路线,这个路线肯定是要有指示箭头的,为了画这个箭头,我花了不少于十几个小时,总算做出来了,但始终有点问题。我对这个箭头的要求是,无论场景拉近还是拉远,这个箭头不能太大,也不能太小看不清,形状不能变化,否则就不像箭头了。
使用到了 three.js 的 Line2.js 和一个开源库MeshLine.js
部分代码:
DrawPath.js:
/** * 绘制路线 */ 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 = []; 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); 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 刷新 ====================================================") 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); 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 matLine = new LineMaterial({ color: new THREE.Color(_self.color), line 0.003, // in world units with size attenuation, pixels otherwise dashed: false, depthTest: _self._depthTest, side: _side, vertexColors: THREE.VertexColors }); 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) { var meshLine = _self.createArrowLine(point1, point2); let canvasTexture = _canvasDraw.drawArrow(THREE, renderer, 300, 100); //箭头 var material = new MeshLineMaterial({ useMap: true, map: canvasTexture, color: new THREE.Color(_self.color), opacity: 1, resolution: new THREE.Vector2($(viewerContainerId).width(), $(viewerContainerId).height()), lineWidth: 50, depthTest: _self._depthTest, side: _side, repeat: new THREE.Vector2(1, 1), transparent: true, sizeAttenuation: 0 }); 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) { var meshLine = _self.createArrowLine(point1, point2); let canvasTexture = _canvasDraw.drawArrow(THREE, renderer, 300, 100); //箭头 var material = new MeshLineMaterial({ useMap: true, map: canvasTexture, color: new THREE.Color(_self.color), opacity: 1, resolution: new THREE.Vector2($(viewerContainerId).width(), $(viewerContainerId).height()), lineWidth: 50, depthTest: _self._depthTest, side: _side, repeat: new THREE.Vector2(1, 1), transparent: true, sizeAttenuation: 0 }); 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); //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; } this.setDepthTest = function (bl) { if (bl) { _self._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 { _self._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; }); } } this.getPath = function () { return this._path; } this.hide = function () { this._lines.map(line => scene.remove(line)); this._arrows.map(arrow => scene.remove(arrow)); this._hide = true; } this.show = function () { this._lines.map(line => scene.add(line)); this._arrows.map(arrow => scene.add(arrow)); 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); drawArrow(point1, point2); } } } this.getDepthTest = function () { return _self._depthTest; } /** * 撤销 */ 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 }
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.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 }
show.js中的部分代码:
let drawPath; //绘制线路 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);
效果图:
还是有点问题:
虽然这个效果图中,场景拉近,箭头有点大,但是最大大小还是做了控制的,就是这个形状有点问题,可能是视角的问题。
我期望的效果应该是这样的,就是无论从什么角度看,箭头不要变形: