在可视化开发中,无论是二维的 canvas 还是三维开发,线条的绘制都是十分常见的,比方绘制城市之间的迁徙图、静止轨迹图等等。不论是在三维还是二维,所有物体都是由点形成、两点形成线、三点形成面。那么在 ThreeJS 中绘制一根简略的线的背地又有哪些故事呢,本文将逐个解开。
一根线的诞生
在 ThreeJS 中,物体由几何体(Geometry) 和材质(Material) 形成,物体以何种形式(点、线、面)展现取决于渲染形式(ThreeJS 提供了不同的物体构造函数)。
翻看 ThreeJS 的 API,与线相干有这些:
简略来说,ThreeJS 提供了 LineBasicMaterial
和 LineDashedMaterial
两类材质,次要控制线的色彩,宽度等;几何体次要控制线段断点的地位等,次要应用 BufferGeometry 这个根本几何类来创立线的几何体。同时也提供了一些线生成函数来帮忙生成线几何体。
直线
在 API 中提供了 Line
LineLoop
LineSegments
三类线相干的物体
Line
先应用 Line
来创立一根最简略的线:
// 创立材质const material = new THREE.LineBasicMaterial({ color: 0xff0000 });// 创立空几何体const geometry = new THREE.BufferGeometry()const points = [];points.push(new THREE.Vector3(20, 20, 0));points.push(new THREE.Vector3(20, -20, 0));points.push(new THREE.Vector3(-20, -20, 0));points.push(new THREE.Vector3(-20, 20, 0));// 绑定顶点到空几何体geometry.setFromPoints(points);const line = new THREE.Line(geometry, material);scene.add(line);
LineLoop
LineLoop
用于将一系列点绘制成一条间断的线,它和 Line
简直一样,惟一的区别就是所有点连贯之后会将第一个点和最初一个点相连接,这种线条在理论我的项目中用于绘制某个区域,比方在地图上用线条勾选出某一区域。应用 LineLoop
创立一个对象:
// 创立材质const material = new THREE.LineBasicMaterial({ color: 0xff0000 });// 创立空几何体const geometry = new THREE.BufferGeometry()const points = [];points.push(new THREE.Vector3(20, 20, 0));points.push(new THREE.Vector3(20, -20, 0));points.push(new THREE.Vector3(-20, -20, 0));points.push(new THREE.Vector3(-20, 20, 0));// 绑定顶点到空几何体geometry.setFromPoints(points);const line = new THREE.LineLoop(geometry, material);scene.add(line);
同样是四个点,应用 LineLoop
创立后是一个闭合的区域。
LineSegments
LineSegments
用于将两个点连贯为一条线,它会将咱们传递的一系列点主动调配成两个为一组,而后将调配好的两个点连贯,这种先天理论我的项目中次要用于绘制具备雷同开始点,完结点不同的线条,比方罕用到的遗传图。应用 LineSegments
创立一个对象:
// 创立材质const material = new THREE.LineBasicMaterial({ color: 0xff0000 });// 创立空几何体const geometry = new THREE.BufferGeometry()const points = [];points.push(new THREE.Vector3(20, 20, 0));points.push(new THREE.Vector3(20, -20, 0));points.push(new THREE.Vector3(-20, -20, 0));points.push(new THREE.Vector3(-20, 20, 0));// 绑定顶点到空几何体geometry.setFromPoints(points);const line = new THREE.LineSegments(geometry, material);scene.add(line);
区别
上述三个线对象的区别是底层渲染的 WebGL 形式不同,假如有 p1/p2/p3/p4/p5 五个点,
Line
应用的是gl.LINE_STRIP
,画一条直线到下一个顶点,最终连线是 p1- > p2 -> p3 -> p4 -> p5LineLoop
应用的是gl.LINE_LOOP
,绘制一条直线到下一个顶点,并将最初一个顶点返回到第一个顶点,最终连线是 p1- > p2 -> p3 -> p4 -> p5 -> p1LineSegments
应用的是gl.LINES
,在一对顶点之间画一条线,最终连线是 p1- > p2 p3 -> p4
如果仅仅是绘制两个点之间的一条线段,那么上述三种实现形式都是没有什么区别的,实现成果都是一样的。
虚线
除了 LineBasicMaterial
,ThreeJS 还提供了 LineDashedMaterial
这个材质来绘制虚线:
// 虚线材质const material = new THREE.LineDashedMaterial({ color: 0xff0000, scale: 1, dashSize: 3, gapSize: 1,});const points = [];points.push(new THREE.Vector3(10, 10, 0)); points.push(new THREE.Vector3(10, -10, 0)); points.push(new THREE.Vector3(-10, -10, 0)); points.push(new THREE.Vector3(-10, 10, 0));const geometry = new THREE.BufferGeometry().setFromPoints(points);const line = new THREE.Line(geometry, material);// 计算LineDashedMaterial所需的间隔的值的数组。 line.computeLineDistances();scene.add(line);
<img src="https://img.alicdn.com/imgextra/i4/O1CN010B12zS1TwlulbyP9Y_!!6000000002447-2-tps-908-574.png" style="zoom:50%;" />
须要留神的是,绘制虚线须要计算线条之间的间隔,否则不会呈现虚线的成果。 对于几何体中的每一个顶点,line.computeLineDistances
这个办法计算出了以后点到线的起始点的累积长度。
炫酷的线
加点宽度
LineBasicMaterial
提供了设置线宽的 linewidth、相邻线段间的连贯形态 linecap 以及端点形态 linecap,然而设置了之后却发现不失效,ThreeJS 的文档也阐明了这一点:
因为底层 OpenGL 渲染的限制性,线宽的最大和最小值都只能为 1,线宽无奈设置,那么线段之间的连贯形态设置也就没有意义了,因而这三个设置项都是无奈失效的。
ThreeJS 官网提供了一个能够设置线宽的 demo,这个 demo 应用了扩大包 jsm 中的材质 LineMaterial
、几何体 LineGeometry
和对象 Line2
。
import { Line2 } from './jsm/lines/Line2.js';import { LineMaterial } from './jsm/lines/LineMaterial.js';import { LineGeometry } from './jsm/lines/LineGeometry.js';const geometry = new LineGeometry();geometry.setPositions( positions );const matLine = new LineMaterial({ color: 0xffffff, linewidth: 5, // in world units with size attenuation, pixels otherwise //resolution: // to be set by renderer, eventually dashed: false, alphaToCoverage: true,});const line = new Line2(geometry, matLine);line.computeLineDistances();line.scale.set(1, 1, 1);scene.add( line );function animate() { renderer.render(scene, camera); // renderer will set this eventually matLine.resolution.set( window.innerWidth, window.innerHeight ); // resolution of the viewport requestAnimationFrame(animate);}
须要留神的是,在渲染循环的 loop 中,每帧都须要从新设置材质的 resolution
,否则宽度成果就无奈失效;Line2
没有提供文档阐明,具体参数须要通过观察源码进行摸索。
加点色彩
在根本 demo 中,通过材质的 color
来对立设置线的色彩,那么如果想实现突变成果又该如何实现呢?
在材质设置中, vertexColors
这个参数能够管制材质色彩的起源,如果设置为 true,那么色彩的计算逻辑来自于顶点色彩,通过肯定的插值平滑过渡为间断的色彩变动。
// 创立材质const material = new THREE.LineMaterial({ linewidth: 2, vertexColors: true, resolution: new THREE.Vector2(800, 600),});// 创立空几何体const geometry = new THREE.LineGeometry();geometry.setPositions([ 10,10,0, 10,-10,0, -10,-10,0, -10,10,0]);// 设置顶点色彩geometry.setColors([ 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0]);const line = new THREE.Line2(geometry, material);line.computeLineDistances();scene.add(line);
上述代码创立了四个点,别离设置顶点色彩为红色(1,0,0)、绿色(0,1,0)、蓝色(0,0,1)、黄色(1,1,0),失去的渲染成果如下图:
这个例子只设置了四个顶点的色彩,如果色彩的插值函数距离获得更小,咱们就能创立出细节更丰盛的色彩。
加点形态
两点相连能够指定一根线,如果点与点之间的间距十分小,而点又十分密集时,点点之间相连即能够生成各式各样的曲线了。
ThreeJS 提供了多种曲线生成函数,次要分为二维曲线和三维曲线:
<img src="https://img.alicdn.com/imgextra/i3/O1CN01zjHrBJ1cn00O1kmjD_!!6000000003644-2-tps-476-524.png" style="zoom:50%;" />
ArcCurve
和EllipseCurve
别离绘制圆和椭圆的,EllipseCurve
是ArcCurve
的基类;LineCurve
和LineCurve3
别离绘制二维和三维的曲线(数学曲线的定义包含直线),他们都由起始点和终止点组成;QuadraticBezierCurve
、QuadraticBezierCurve3
、CubicBezierCurve
和CubicBezierCurve3
别离是二维、三维、二阶、三阶贝塞尔曲线;SplineCurve
和CatmullRomCurve3
别离是二维和三维的样条曲线,应用 Catmull-Rom 算法,从一系列的点创立一条平滑的样条曲线。
贝塞尔曲线与 CatmullRom 曲线的区别在于,CatmullRom 曲线能够平滑的通过所有点,个别用于绘制轨迹,而贝塞尔曲线通过两头点来结构切线。
- 贝塞尔曲线
- CatmullRom 曲线
这些构造函数通过参数生成曲线,Curve
基类提供了 getPoints
办法类获取曲线上的点,参数为曲线划分段数,段数越多,划分越密,点越多,曲线越润滑。最初将这系列点并赋值到几何体中,以贝塞尔曲线为例:
// 创立几何体const geometry = new THREE.BufferGeometry();// 创立曲线const curve = new THREE.CubicBezierCurve3( new THREE.Vector3(-10, -20, -10), new THREE.Vector3(-10, 40, -10), new THREE.Vector3(10, 40, 10), new THREE.Vector3(10, -20, 10));// getPoints 办法从曲线中获取点const points = curve.getPoints(100);// 将这系列点赋值给几何体geometry.setFromPoints(points);// 创立材质const material = new THREE.LineBasicMaterial({color: 0xff0000});const line = new THREE.Line(geometry, material);scene.add(line);
<img src="https://img.alicdn.com/imgextra/i3/O1CN01mLGaXQ1WeOsF7cHVJ_!!6000000002813-2-tps-852-859.png" style="zoom:50%;" />
咱们也能够通过继承 Curve
基类,通过重写基类中 getPoint
办法来实现自定义曲线,getPoint
办法是返回在曲线中给定地位 t 的向量。
比方实现一条正弦函数的曲线:
class CustomSinCurve extends THREE.Curve { constructor( scale = 1 ) { super(); this.scale = scale; } getPoint( t, optionalTarget = new THREE.Vector3() ) { const tx = t * 3 - 1.5; const ty = Math.sin( 2 * Math.PI * t ); const tz = 0; return optionalTarget.set( tx, ty, tz ).multiplyScalar( this.scale ); }}
加点拉伸
线不论如何变动都只是二维立体,尽管上述有一些三维曲线,不过是法平面不同。如果咱们想模仿一些相似管道的成果,管道是有直径的概念,那么二维线必定无奈满足要求。所以咱们须要应用其余几何体来实现管道成果。
ThreeJS 封装了很多几何体供咱们应用,其中就有一个 TubeGeometry
管道几何体,
它能够依据 3d 曲线往外拉伸出一条管道,它的构造函数:
class TubeGeometry(path : Curve, tubularSegments : Integer, radius : Float, radialSegments : Integer, closed : Boolean)
path 即是曲线,形容管道形态。咱们应用后面本人创立的正弦函数曲线CustomSinCurve
来生成一条曲线,并应用 TubeGeometry
拉伸。
const tubeGeometry = new THREE.TubeGeometry(new CustomSinCurve(10), 20, 2, 8, false);const tubeMaterial = new THREE.MeshStandardMaterial({ color: 0x156289, emissive: 0x072534, side: THREE.DoubleSide });const tube = new THREE.Mesh(tubeGeometry, tubeMaterial);scene.add(tube)
加点动画
到这个时候,咱们的线曾经有了宽度、色彩、形态,那么下一步该动起来了!动起来的本质是在每个渲染帧扭转物体的某个属性,造成肯定的间断成果,所以咱们有两个思路去让线条动起来,一种是让线的几何体动起来,一种是让线的材质动起来,
流动的线
在材质动画中,应用最为频繁的是贴图流动。通过设置贴图的 repeat
属性,并一直扭转贴图对象的 offset
让贴图产生流动成果。
如果要在线中实现贴图流动成果,二维的线是无奈实现的,必须要在拉伸后的三维管道中才有意义。同样应用前述实现的管道体,而后对材质赋予贴图配置:
// 创立纹理const imgUrl = 'xxx'; // 图片地址const texture = new THREE.TextureLoader().load(imgUrl);texture.wrapS = THREE.RepeatWrapping;texture.wrapT = THREE.RepeatWrapping;// 管制纹理反复参数texture.repeat.x = 10;texture.repeat.y = 1;// 将纹理利用于材质const tubeMaterial = new THREE.MeshStandardMaterial({ color: 0x156289, emissive: 0x156289, map: texture, side: THREE.DoubleSide,});const tube = new THREE.Mesh(tubeGeometry, tubeMaterial);scene.add(tube)function renderLoop() { const delta = clock.getDelta(); renderer.render(scene, camera); // 在renderloop中更新纹理的offset if (texture) { texture.offset.x -= 0.01; } requestAnimationFrame(renderLoop);}
demo
成长的线
成长的线的实现思路很简略,先计算定义好一系列点,即线的最终形态,而后再创立一条只有前两个点的线,而后向创立好的线外面按程序塞入其余点,再更新这条线,最终就能失去线成长的成果。
BufferGeometry 的更新
在此之前,咱们再次来理解一下 ThreeJS 中的几何体。ThreeJS 中的几何体能够分为,点Points、线Line、网格Mesh。Points 模型创立的物体是由一个个点形成,每个点都有本人的地位,Line 模型创立的物体是间断的线条,这些线能够了解为是按程序把所有点连接起来, Mesh 网格模型创立的物体是由一个个小三角形组成,这些小三角形又是由三个点确定。不论是哪一种模型,它们都有一个共同点,就是都离不开点,每一个点都有确定的 x y z,BoxGeometry、SphereGeometry 帮咱们封装了对这些点的操作,咱们只须要通知它们长宽高或者半径这些信息,它就会帮我创立一个默认的几何体。而 BufferGeometry 就是齐全由咱们本人去操作点信息的办法,咱们能够通过它去设置每一个点的地位(position)、每一个点的色彩(color)、每一个点的法向量(normal) 等。
与 Geometry 相比,BufferGeometry 将信息(例如顶点地位,面索引,法线,色彩,uv和任何自定义属性)存储在 buffer 中 —— 也就是 Typed Arrays。这使得它们通常比规范 Geometry 更快,但毛病是更难用。
在更新 BufferGeometry 时,最重要的一个点是,不能调整 buffer 的大小,这种操作开销很大,相当于创立了个新的 geometry,但能够更新 buffer 的内容。所以如果冀望 BufferGeometry 的某个属性会减少,比方顶点的数量,必须事后调配足够大的 buffer 来包容可能创立的任意新顶点数。 当然,这也意味着 BufferGeometry 将有一个最大大小,也就是无奈创立一个能够高效有限扩大的 BufferGeometry。
那么,在绘制成长的线时,理论问题就是在渲染时扩大线的顶点。举个例子,咱们先为 BufferGeometry 的顶点属性调配可包容 500 个顶点的缓冲区,但最后只绘制 2 个,再通过 BufferGeometry 的 drawRange
办法来管制绘制的缓冲区范畴。
const MAX_POINTS = 500;// 创立几何体const geometry = new THREE.BufferGeometry();// 设置几何体的属性const positions = new Float32Array( MAX_POINTS * 3 ); // 一个顶点向量须要3个地位形容geometry.setAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );// 管制绘制范畴const drawCount = 2; // 只绘制前两个点geometry.setDrawRange( 0, drawCount );// 创立材质const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );// 创立线const line = new THREE.Line( geometry, material );scene.add(line);
而后随机增加顶点到线中:
const positions = line.geometry.attributes.position.array;let x, y, z, index;x = y = z = index = 0;for ( let i = 0; i < MAX_POINTS; i ++ ) { positions[ index ++ ] = x; positions[ index ++ ] = y; positions[ index ++ ] = z; x += ( Math.random() - 0.5 ) * 30; y += ( Math.random() - 0.5 ) * 30; z += ( Math.random() - 0.5 ) * 30;}
如果要更改第一次渲染后渲染的点数,执行以下操作:
line.geometry.setDrawRange(0, newValue);
如果要在第一次渲染后更改 position 数值,则须要设置 needsUpdate 标记:
line.geometry.attributes.position.needsUpdate = true; // 须要加在第一次渲染之后
demo
画线
在三维搭建场景下的编辑器中,常常须要绘制物体与物体之间的连贯,例如工业场景中绘制管道、建模场景中绘制货架等等。这个过程能够形象为在屏幕上点击两点生成一条直线。在二维场景下,这个性能听起来没有任何难度,然而在三维场景中,又该如何实现呢?
首先要解决的是线的顶点更新,即鼠标点击一次确定线中的一个顶点,再次点击确定下一个顶点地位,其次要解决的是三维场景下点击与交互问题,如何在二维屏幕中确定三维点地位,如何保障用户点击的点就是其所了解的地位。
LineGeometry 的更新
在绘制一般的线时,几何体都应用了 BufferGeometry,咱们也在上一大节介绍了如何对其进行更新。但在绘制有宽度的线这一节中,咱们应用了扩大包 jsm 中的材质 LineMaterial
、几何体 LineGeometry
和对象 Line2
。LineGeometry 又该如何更新呢?
LineGeometry 提供了 setPosition
的办法,对其 BufferAttribute 进行操作,因而咱们不须要关怀如何更新
翻看源码能够晓得,LineGeometry 的底层渲染,并不是间接通过 positions 属性来计算地位,而是通过属性 instanceStart
instanceEnd
来设置的。LineGeometry 提供了 setPositions
办法来更新线的顶点。
class LineSegmentsGeometry { // ... setPositions( array ) { let lineSegments; if ( array instanceof Float32Array ) { lineSegments = array; } else if ( Array.isArray( array ) ) { lineSegments = new Float32Array( array ); } const instanceBuffer = new InstancedInterleavedBuffer( lineSegments, 6, 1 ); // xyz, xyz this.setAttribute( 'instanceStart', new InterleavedBufferAttribute( instanceBuffer, 3, 0 ) ); // xyz this.setAttribute( 'instanceEnd', new InterleavedBufferAttribute( instanceBuffer, 3, 3 ) ); // xyz this.computeBoundingBox(); this.computeBoundingSphere(); return this; }}
因而绘制时咱们只须要调用 setPositions
办法来更新线顶点,同时须要事后定好绘制线最大可包容的顶点数,再管制渲染范畴,实现思路同上。
const MaxCount = 10;const positions = new Float32Array(MaxCount * 3);const points = [];const material = new THREE.LineMaterial({ linewidth: 2, color: 0xffffff, resolution: new THREE.Vector2(800, 600)});geometry = new THREE.LineGeometry();geometry.setPositions(positions);geometry.instanceCount = 0;line = new THREE.Line2(geometry, material);line.computeLineDistances();scene.add(line);// 鼠标挪动或点击时更新线function updateLine() { positions[count * 3 - 3] = mouse.x; positions[count * 3 - 2] = mouse.y; positions[count * 3 - 1] = mouse.z; geometry.setPositions(positions); geometry.instanceCount = count - 1;}
点击与交互
在三维场景下如何实现点选交互呢?鼠标所在的屏幕是一个二维的世界,而屏幕出现的是一个三维世界,首先先解释一下三种坐标系的关系:世界坐标系、屏幕坐标系、视点坐标系。
场景坐标系(世界坐标系)
通过 ThreeJS 构建进去的场景,都具备一个固定不变的坐标系(无论相机的地位在哪),并且搁置的任何物体都要以这个坐标系来确定本人的地位,也就是
(0,0,0)
坐标。例如咱们创立一个场景并增加箭头辅助。屏幕坐标
在显示屏上的坐标就是屏幕坐标系。如下图所示,其中的
clientX
和clientY
的最值由,window.innerWidth
,window.innerHeight
决定。视点坐标
视点坐标系就是以相机的中心点为原点,然而相机的地位,也是依据世界坐标系来偏移的,WebGL 会将世界坐标先变换到视点坐标,而后进行裁剪,只有在眼帘范畴(视见体)之内的场景才会进入下一阶段的计算
如下图增加了相机辅助线.
如果想获取鼠标点击的坐标,就须要把屏幕坐标系转换为 ThreeJS 中的场景坐标系。一种是采纳几何相交性计算的形式,从鼠标点击的中央,沿着视角方向发射一条射线。通过射线与三维模型的几何相交性判断来决定物体是否被拾取到。 ThreeJS 内置了一个 Raycaster
的类,为咱们提供的是一个射线,而后咱们能够依据不同的方向去发射射线,依据射线是否被阻挡,来判断咱们是否碰到了物体。来看看如何应用 Raycaster类来实现鼠标点击物体的高亮显示成果
const raycaster = new THREE.Raycaster();const mouse = new THREE.Vector2();renderer.domElement.addEventListener("mousedown", (event) => { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(cubes, true); if (intersects.length > 0) { var obj = intersects[0].object; obj.material.color.set("#ff0000"); obj.material.needsUpdate= true; }})
实例化 Raycaster
对象,以及一个记录鼠标地位的二维向量 mouse
。当监听 dom 节点mousedown
事件被触发的时候,能够在事件回调外面,获取到鼠标在以后 dom 上的地位 (event.clientX、event.clientY)
。而后把屏幕坐标转化为 场景坐标系中的屏幕坐标地位。对应关系如下图所示。
屏幕坐标系的原点为左上角,Y 轴向下,而三维坐标系的原点是屏幕核心,Y 轴向上且做了归一化解决,因而如果要讲鼠标地位 x 换算到三维坐标系中:
1.将原点转到屏幕两头即 x - 0.5*canvasWidth2.做归一化解决 (x - 0.5*canvasWidth)/(0.5*canvasWidth)即最终 (event.clientX / window.innerWidth) * 2 - 1;
y 轴计算同理,不过做了一次翻转。
持续调用 raycaster 的 setFromCamera
办法,能够取得一条和相机朝向统一、从鼠标点射进来的射线。而后调用射线与物体相交的检测函数 intersectObjects
。
class Raycaster { // ... intersectObjects(objects: Object3D[], recursive?: boolean, optionalTarget?: Intersection[]): Intersection[];}
第一个参数 objects
是检测与射线相交的一组物体,第二个参数 recursive
默认只检测以后级别的物体,子物体不做检测。如果须要查看所有后辈,须要显示设置为 true。
- 在画线中的交互限度
在画线场景下,点击两点确定一条直线,然而在二维屏幕内去看三维世界,人感触到的三维坐标并不一定是理论的三维坐标,如果画线交互须要更加准确,即保障鼠标点击的点就是用户了解的三维坐标点,那么须要加一些限度。
因为在二维屏幕内能够准确确定一个点的地位,那么如果咱们把射线拾取范畴限度在一个固定立体内呢?即先确定立体,再确定点的地位。进入下一个点绘制前,能够切换立体。通过限度拾取范畴,保障鼠标点击的点是用户了解的三维坐标点。
简略起见,咱们创立三个根底拾取立体 XY/XZ/YZ,绘制一个点时拾取立体是确定的,同时创立辅助网格线来帮忙用户察看本人是在哪个立体内绘制。
const planeMaterial = new THREE.MeshBasicMaterial();const planeGeometry = new THREE.PlaneGeometry(100, 100);// XY 立体 即在 Z 方向上绘制const planeXY = new THREE.Mesh(planeGeometry, planeMaterial);planeXY.visible = false;planeXY.name = "planeXY";planeXY.rotation.set(0, 0, 0);scene.add(planeXY);// XZ 立体 即在 Y 方向上绘制const planeXZ = new THREE.Mesh(planeGeometry, planeMaterial);planeXZ.visible = false;planeXZ.name = "planeXZ";planeXZ.rotation.set(-Math.PI / 2, 0, 0);scene.add(planeXZ);// YZ 立体 即在 X 方向上绘制const planeYZ = new THREE.Mesh(planeGeometry, planeMaterial);planeYZ.visible = false;planeYZ.name = "planeYZ";planeYZ.rotation.set(0, Math.PI / 2, 0);scene.add(planeYZ);// 辅助网格const grid = new THREE.GridHelper(10, 10);scene.add(grid);// 初始化设置mode = "XZ";grid.rotation.set(0, 0, 0);activePlane = planeXZ;// 设置拾取立体
- 鼠标挪动时 更新地位
在鼠标挪动时,用射线获取鼠标点与拾取立体的坐标,作为线的下一个点地位:
function handleMouseMove(event) { if (drawEnabled) { const { clientX, clientY } = event; const rect = container.getBoundingClientRect(); mouse.x = ((clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -(((clientY - rect.top) / rect.height) * 2) + 1; raycaster.setFromCamera(mouse, camera); // 计算射线与以后立体的交叉点 const intersects = raycaster.intersectObjects([activePlane], true); if (intersects.length > 0) { const intersect = intersects[0]; const { x: x0, y: y0, z: z0 } = lastPoint; const x = Math.round(intersect.point.x); const y = Math.round(intersect.point.y); const z = Math.round(intersect.point.z); const newPoint = new THREE.Vector3(); if (mode === "XY") { newPoint.set(x, y, z0); } else if (mode === "YZ") { newPoint.set(x0, y, z); } else if (mode === "XZ") { newPoint.set(x, y0, z); } mouse.copy(newPoint); updateLine(); } }}
- 鼠标点击时 增加点
鼠标点击后,以后点被正式增加到线中,并作为上一个顶点记录,同时更新拾取立体与辅助网格的地位。
function handleMouseClick() { if (drawEnabled) { const { x, y, z } = mouse; positions[count * 3 + 0] = x; positions[count * 3 + 1] = y; positions[count * 3 + 2] = z; count += 1; grid.position.set(x, y, z); activePlane.position.set(x, y, z); lastPoint = mouse.clone(); }}
- 键盘切换模式
为不便起见,监听键盘事件来管制模式,X/Y/Z 别离切换不同的拾取立体,D/S 来管制画线是否能够操作。
function handleKeydown(event) { if (drawEnabled) { switch (event.key) { case "d": drawEnabled = false; break; case "s": drawEnabled = true; break; case "x": mode = "YZ"; grid.rotation.set(-Math.PI / 2, 0, 0); activePlane = planeYZ; break; case "y": mode = "XZ"; grid.rotation.set(0, 0, 0); activePlane = planeXZ; break; case "z": mode = "XY"; grid.rotation.set(0, 0, Math.PI / 2); activePlane = planeXY; break; default: } }}
最初实现的成果
Demo
如果稍加拓展,能够对交互进行更粗疏的优化,也能够在生成线之后对线材质的相干属性进行编辑,能够玩的花色就十分多了。
总结
线在图形绘制中始终是一个十分有意思的话题,可延长的技术点也很多。从 OpenGL 中根本的线连贯形式,到为线加一些宽度、色彩等成果,以及在编辑场景下如何实现画线性能。上述对 ThreeJS 中线的总结如果有任何问题,都欢送一起探讨!
作者:ES2049 | Dell
文章可随便转载,但请保留原文链接。
十分欢送有激情的你退出 ES2049 Studio,简历请发送至 caijun.hcj@alibaba-inc.com 。