在可视化开发中,无论是二维的 canvas 还是三维开发,线条的绘制都是十分常见的,比方绘制城市之间的迁徙图、静止轨迹图等等。不论是在三维还是二维,所有物体都是由点形成、两点形成线、三点形成面。那么在 ThreeJS 中绘制一根简略的线的背地又有哪些故事呢,本文将逐个解开。

一根线的诞生

在 ThreeJS 中,物体由几何体(Geometry) 和材质(Material) 形成,物体以何种形式(点、线、面)展现取决于渲染形式(ThreeJS 提供了不同的物体构造函数)。

翻看 ThreeJS 的 API,与线相干有这些:

简略来说,ThreeJS 提供了 LineBasicMaterialLineDashedMaterial 两类材质,次要控制线的色彩,宽度等;几何体次要控制线段断点的地位等,次要应用 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 -> p5
  • LineLoop 应用的是 gl.LINE_LOOP,绘制一条直线到下一个顶点,并将最初一个顶点返回到第一个顶点,最终连线是 p1- > p2 -> p3 -> p4 -> p5 -> p1
  • LineSegments 应用的是 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%;" />

  • ArcCurveEllipseCurve 别离绘制圆和椭圆的,EllipseCurveArcCurve 的基类;
  • LineCurveLineCurve3 别离绘制二维和三维的曲线(数学曲线的定义包含直线),他们都由起始点和终止点组成;
  • QuadraticBezierCurveQuadraticBezierCurve3CubicBezierCurveCubicBezierCurve3 别离是二维、三维、二阶、三阶贝塞尔曲线;
  • SplineCurveCatmullRomCurve3 别离是二维和三维的样条曲线,应用 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) 坐标。例如咱们创立一个场景并增加箭头辅助。

  • 屏幕坐标

    在显示屏上的坐标就是屏幕坐标系。如下图所示,其中的 clientXclientY 的最值由,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 。