Threejs入门教程和一些案例,看这一篇就够了!!

1,877 阅读52分钟

快速入门

1.学习环境和引入Threejs

项目的开发环境引入threejs

// 比如安装148版本
npm install three@0.148.0 --save

npm安装后,如何引入three.js

// 引入three.js
import * as THREE from 'three';

npm安装后,如何引入three.js其他扩展库

// 引入扩展库OrbitControls.js
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 引入扩展库GLTFLoader.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

2. 第一个3D案例

入门Three.js的第一步,就是认识场景Scene相机Camera渲染器Renderer三个基本概念。

image.png

(1)创建3D场景

三维场景Scene

三维场景(Scene)对象理解为虚拟的3D场景,用来表示模拟生活中的真实三维场景,或者说三维世界。

// 创建3D场景对象Scene
const scene = new THREE.Scene();

物体形状:几何体Geometry

image.png

//创建一个长方体几何对象Geometry
const geometry = new THREE.BoxGeometry(100, 100, 100); 

物体外观:材质Material

image.png

//创建一个材质对象Material
const material = new THREE.MeshBasicMaterial({
    color: 0xff0000,//0xff0000设置材质颜色为红色
}); 

物体:网格模型Mesh

实际生活中有各种各样的物体,在threejs中可以通过网格模型表示一个虚拟的物体,比如一个地板砖、一个房子。

// 两个参数分别为几何体geometry、材质material
const mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh

模型位置.position

const mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
//设置网格模型在三维空间中的位置坐标,默认是坐标原点
mesh.position.set(0,10,0);

.add()方法

在threejs中创建了一个表示物体的虚拟对象Mesh,需要通过.add()方法,把网格模型mesh添加到三维场景scene中。

scene.add(mesh); 

(2)透视投影相机

Threejs如果想把三维场景Scene渲染到web网页上,还需要定义一个虚拟相机Camera,就像你生活中想获得一张照片,需要一台用来拍照的相机。

Threejs提供了正投影相机OrthographicCamera和透视投影相机PerspectiveCamera。根据场景对相机进行选择。

透视投影相机PerspectiveCamera

本质上就是在模拟人眼观察这个世界的规律。

// 实例化一个透视投影相机对象
const camera = new THREE.PerspectiveCamera();

相机位置.position

生活中用相机拍照,你相机位置不同,拍照结果也不同,threejs中虚拟相机同样如此。

比如有一间房子,你拿着相机站在房间里面,看到的是房间内部,站在房子外面看到的是房子外面效果。

//相机在Three.js三维坐标系中的位置
// 根据需要设置相机位置具体值
camera.position.set(200, 200, 200); 

相机观察目标.lookAt()

用相机拍照你需要控制相机的拍照目标,具体说相机镜头对准哪个物体或说哪个坐标。对于threejs相机而言,就是设置.lookAt()方法的参数,指定一个3D坐标。

//相机观察目标指向Threejs 3D空间中某个位置
camera.lookAt(0, 0, 0); //坐标原点
camera.lookAt(mesh.position);//指向mesh对应的位置

image.png

定义相机渲染输出的画布尺寸

生活中相机拍照的照片是有大小的,对于threejs而言一样,需要定义相机在网页上输出的Canvas画布(照片)尺寸,大小可以根据需要定义。

threejs虚拟相机渲染三维场景在浏览器网页上呈现的结果称为Canvas画布

// 定义相机输出画布的尺寸(单位:像素px)
const width = 800; //宽度
const height = 500; //高度

透视投影相机PerspectiveCamera:视锥体

透视投影相机的四个参数fov, aspect, near, far构成一个四棱台3D空间,被称为视锥体,只有视锥体之内的物体,才会渲染出来,视锥体范围之外的物体不会显示在Canvas画布上。

image.png

PerspectiveCamera( fov, aspect, near, far )

image.png

(3)渲染器

生活中如果有了景物相机,那么如果想获得一张照片,就需要你拿着相机,按一下,咔,完成拍照。对于threejs而言,如果完成“咔”这个拍照动作,就需要一个新的对象,也就是WebGL渲染(WebGLRenderer)。

WebGL渲染器WebGLRenderer

// 创建渲染器对象
const renderer = new THREE.WebGLRenderer();

设置Canvas画布尺寸.setSize()

// 定义threejs输出画布的尺寸(单位:像素px)
const width = 800; //宽度
const height = 500; //高度
renderer.setSize(width, height); //设置three.js渲染区域的尺寸(像素px)

渲染器渲染方法.render()

渲染器WebGLRenderer执行渲染方法.render()就可以生成一个Canvas画布(照片),并把三维场景Scene呈现在canvas画布上面。

renderer.render(scene, camera); //执行渲染操作

渲染器Canvas画布属性.domElement

渲染器WebGLRenderer通过属性.domElement可以获得渲染方法.render()生成的Canvas画布,.domElement本质上就是一个HTML元素:Canvas画布。

document.body.appendChild(renderer.domElement);

Canvas画布插入到任意HTML元素中

<div id="webgl" style="margin-top: 200px;margin-left: 100px;"></div>

document.getElementById('webgl').appendChild(renderer.domElement);

3. 三维坐标系-加强三维空间认识

辅助观察坐标系

// AxesHelper:辅助观察的坐标系
const axesHelper = new THREE.AxesHelper(150);
scene.add(axesHelper);

AxesHelper的xyz轴

three.js坐标轴颜色红R、绿G、蓝B分别对应坐标系的xyz轴,对于three.js的3D坐标系默认y轴朝上

image.png

4. 光源对物体表面影响

实际生活中物体表面的明暗效果是会受到光照的影响,threejs中同样也要模拟光照Light对网格模型Mesh表面的影响。

受光照影响材质

image.png

//MeshBasicMaterial不受光照影响
const material = new THREE.MeshBasicMaterial(); 

//MeshLambertMaterial受光照影响
const material = new THREE.MeshLambertMaterial(); 

光源简介

image.png

image.png 点光源可以类比为一个发光点,就像生活中一个灯泡以灯泡为中心向四周发射光线。

//点光源:两个参数分别表示光源颜色和光照强度
const pointLight = new THREE.PointLight(0xffffff, 1.0);

//环境光:没有特定方向,整体改变场景的光照明暗
const ambient = new THREE.AmbientLight(0xffffff, 0.4);

// 平行光
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
// 方向光指向对象网格模型mesh,可以不设置,默认的位置是0,0,0
directionalLight.target = mesh;

光源位置

可以把点光源想象为一个电灯泡,在3D空间中放的位置不同,模型的渲染效果就不一样。

pointLight.position.set(100, 60, 50); 

光源辅助器

// DirectionalLightHelper:可视化平行光
const dirLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5,0xff0000);
scene.add(dirLightHelper);

光源添加到场景

光源和网格模型Mesh对应一样是三维场景的一部分,自然需要添加到三维场景中才能起作用。

scene.add(pointLight); //点光源添加到场景中

5. 相机控件OrbitControls

平时开发调试代码,或者展示模型的时候,可以通过相机控件OrbitControls实现旋转缩放预览效果。

  • 旋转:拖动鼠标左键
  • 缩放:滚动鼠标中键
  • 平移:拖动鼠标右键
// 引入
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 使用
const controls = new OrbitControls(camera, renderer.domElement);
// 如果OrbitControls改变了相机参数,重新调用渲染器渲染三维场景
controls.addEventListener('change', function () {
    renderer.render(scene, camera); //执行渲染操作
});

6. 动画渲染循环

threejs可以借助HTML5的API请求动画帧window.requestAnimationFrame实现动画渲染。

请求动画帧window.requestAnimationFrame

// requestAnimationFrame实现周期性循环执行
let i = 0;
function render() {
    i+=1;
    console.log('执行次数'+i);
    requestAnimationFrame(render);//请求再次执行函数render
}
render();

7. Canvas画布布局和全屏

threejs渲染输出的结果就是一个Cavnas画布,canvas画布也是HTML的元素之一,这意味着three.js渲染结果的布局和普通web前端习惯是一样的。

非全屏局部布局

<div id="webgl" style="margin-top: 100px;margin-left: 200px;"></div>


// width和height用来设置Three.js输出的Canvas画布尺寸(像素px)
const width = 800; //宽度
const height = 500; //高度

const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);

const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height); //设置three.js渲染区域的尺寸(像素px)
renderer.render(scene, camera); //执行渲染操作
document.getElementById('wegbl').appendChild(renderer.domElement);

全屏渲染

// width和height用来设置Three.js输出的Canvas画布尺寸(像素px)
const width = window.innerWidth; //窗口文档显示区的宽度作为画布宽度
const height = window.innerHeight; //窗口文档显示区的高度作为画布高度
const renderer = new THREE.WebGLRenderer();
document.body.appendChild(renderer.domElement);

canvas画布宽高度动态变化

canvas画布宽高度动态变化,需要更新相机和渲染的参数,否则无法正常渲染。

// onresize 事件会在窗口被调整大小时发生
window.onresize = function () {
    // 重置渲染器输出画布canvas尺寸
    renderer.setSize(window.innerWidth, window.innerHeight);
    // 全屏情况下:设置观察范围长宽比aspect为窗口宽高比
    camera.aspect = window.innerWidth / window.innerHeight;
   //如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
    camera.updateProjectionMatrix();
};

8. stats查看threejs渲染帧率

three.js每执行WebGL渲染器.render()方法一次,就在canvas画布上得到一帧图像,不停地周期性执行.render()方法就可以更新canvas画布内容,一般场景越复杂往往渲染性能越低,也就是每秒钟执行.render()的次数越低。

通过stats.js库可以查看three.js当前的渲染性能,具体说就是计算three.js的渲染帧率(FPS),所谓渲染帧率(FPS),简单说就是three.js每秒钟完成的渲染次数,一般渲染达到每秒钟60次为最佳状态。

//引入
import Stats from 'three/addons/libs/stats.module.js';

//使用
const stats = new Stats();
document.body.appendChild(stats.domElement);
// 渲染函数
function render() {
	//requestAnimationFrame循环调用的函数中调用方法update(),来刷新时间
	stats.update();
}
render();

9. Threejs常见几何体简介

image.png

//BoxGeometry:长方体
const geometry = new THREE.BoxGeometry(100, 100, 100);
// SphereGeometry:球体
const geometry = new THREE.SphereGeometry(50);
// CylinderGeometry:圆柱
const geometry = new THREE.CylinderGeometry(50,50,100);
// PlaneGeometry:矩形平面
const geometry = new THREE.PlaneGeometry(100,50);
// CircleGeometry:圆形平面
const geometry = new THREE.CircleGeometry(50);

双面可见

Three.js的材质默认正面可见,反面不可见,对于矩形平面PlaneGeometry圆形平面如果你想看到两面,可以设置side: THREE.DoubleSide

side: ThREE.BackSide   //背面可见
side: THREE.FrontSide, //正面可见(默认)
side: THREE.DoubleSide, //两面可见

new THREE.MeshBasicMaterial({
    side: THREE.FrontSide,
});

10. WebGL渲染器设置(锯齿模糊)

一般实际开发,threejs的WebGL渲染器需要进行一些通用的基础配置,比如渲染模糊或锯齿问题。

渲染器锯齿属性.antialias

const renderer = new THREE.WebGLRenderer({
  antialias:true,
});

设置设备像素比.setPixelRatio()

// 不同硬件设备的屏幕的设备像素比window.devicePixelRatio值可能不同
console.log('查看当前屏幕设备像素比',window.devicePixelRatio);

// 获取你屏幕对应的设备像素比.devicePixelRatio告诉threejs,以免渲染模糊问题
renderer.setPixelRatio(window.devicePixelRatio);

11. gui.js库(可视化改变三维场景)

dat.gui.js是一个前端js库,对HTML、CSS和JavaScript进行了封装,学习开发的时候,借助dat.gui.js可以快速创建控制三维场景的UI交互界面。

// 引入dat.gui.js的一个类GUI
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'

// 实例化一个gui对象
const gui = new GUI();

//.add() 功能——改变对象属性值

// 光照强度属性.intensity
gui.add(ambient, 'intensity', 0, 2.0);
// 通过GUI改变mesh.position对象的xyz属性
gui.add(mesh.position, 'x', 0, 180);
gui.add(mesh.position, 'y', 0, 180);
gui.add(mesh.position, 'z', 0, 180);

12. threejs语法总结

Three.js语法总结:类(构造函数)

Three.js提供了各种各样的类(构造函数),通过new关键字可以实例化类(构造函数),获得一个对象,对象具有属性和方法。

// new实例化类THREE.MeshLambertMaterial,创建一个材质对象
const material = new THREE.MeshLambertMaterial();
// 可以看到材质对象的属性color、side、opacity、transparent...
console.log('查看材质对象',material);

类(构造函数)的参数设置属性

const material = new THREE.MeshLambertMaterial({
    color: 0x00ffff, 
    side:THREE.DoubleSide,
    transparent:true,
    opacity:0.5,
});

访问对象属性改变属性的值

// 访问对象属性改变属性的值
material.transparent = false;
material.opacity = 1.0;

父类和子类

JavaScript是一个面向对象的语言,有父类和子类的概念,子类是通过父类派生出来的,会继承父类的属性或方法。

  • 环境光、平行光源的父类Light
  • mesh、light光源的父类Object3D

可以通过文档查询一个类的方法或属性,除了可以查询类本身,还可以查询类的父类。

通过对象的方法改变对象的属性

console.log('模型位置属性',mesh.position);
mesh.position.x = 50;//访问属性改变位置x坐标
mesh.translateX(50);//执行方法改变位置属性

几何体BufferGeometry

1. 几何体顶点位置数据和点模型

缓冲类型几何体BufferGeometry

threejs的长方体BoxGeometry、球体SphereGeometry等几何体都是基于BoxGeometry类构建的,BufferGeometry是一个没有任何形状的空几何体,你可以通过BufferGeometry自定义任何几何形状,具体一点说就是定义顶点数据

//创建一个空的几何体对象
const geometry = new THREE.BufferGeometry(); 

//类型化数组创建顶点数据
const vertices = new Float32Array([
    0, 0, 0, //顶点1坐标
    50, 0, 0, //顶点2坐标
    0, 100, 0, //顶点3坐标
    0, 0, 10, //顶点4坐标
    0, 0, 100, //顶点5坐标
    50, 0, 10, //顶点6坐标
]);

//创建属性缓冲区对象,3个为一组,表示一个顶点的xyz坐标
const attribue = new THREE.BufferAttribute(vertices, 3); 

// 设置几何体attributes属性的位置属性
geometry.attributes.position = attribue;

2. 线模型对象

// 线材质对象
const material = new THREE.LineBasicMaterial({
    color: 0xff0000 //线条颜色
}); 
// 创建线模型对象
const line = new THREE.Line(geometry, material);

// threejs线模型除了Line,还提供了LineLoop,LineSegments,区别在于绘制线条的规则不同。
const line = new THREE.LineLoop(geometry, material);  // 闭合线条
const line = new THREE.LineSegments(geometry, material);//非连续的线条

3. 构建一个矩形平面几何体

网格模型Mesh渲染自定义几何体BufferGeometry的顶点坐标,通过这样一个例子帮助大家建立**三角形(面)**的概念

image.png

定义矩形几何体顶点坐标

一个矩形平面,可以至少通过两个三角形拼接而成。而且两个三角形有两个顶点的坐标是重合的。

注意三角形的正反面问题:保证矩形平面两个三角形的正面是一样的,也就是从一个方向观察,两个三角形都是逆时针或顺时针。 image.png

const vertices = new Float32Array([
    0, 0, 0, //顶点1坐标
    80, 0, 0, //顶点2坐标
    80, 80, 0, //顶点3坐标

    0, 0, 0, //顶点4坐标   和顶点1位置相同
    80, 80, 0, //顶点5坐标  和顶点3位置相同
    0, 80, 0, //顶点6坐标
]);

几何体顶点索引数据

网格模型Mesh对应的几何体BufferGeometry,拆分为多个三角后,很多三角形重合的顶点位置坐标是相同的,这时候如果想减少顶点坐标数据量,可以借助几何体顶点索引geometry.index来实现。

如果几何体有顶点索引geometry.index,那么你可以吧三角形重复的顶点位置坐标删除。

const vertices = new Float32Array([
    0, 0, 0, //顶点1坐标
    80, 0, 0, //顶点2坐标
    80, 80, 0, //顶点3坐标
    0, 80, 0, //顶点4坐标
]);

image.png

BufferAttribute定义顶点索引.index数据

// Uint16Array类型数组创建顶点索引数据
const indexes = new Uint16Array([
    // 下面索引值对应顶点位置数据中的顶点坐标
    0, 1, 2, 0, 2, 3,
])

// 索引数据赋值给几何体的index属性
geometry.index = new THREE.BufferAttribute(indexes, 1); //1个为一组

4. 查看threejs自带几何体顶点

three.js提供的矩形平面PlaneGeometry、长方体BoxGeometry、球体SphereGeometry等各种形状的几何体,他们都有一个共同的父类BufferGeometry

查看几何体顶点位置和索引数据

可以用顶点索引index数据构建几何体,也可以不用,threejs默认的大部分几何体都有三角形的顶点索引数据,具体可以通过浏览器控制台打印几何体数据查看。

const geometry = new THREE.PlaneGeometry(100,50); //矩形平面几何体

console.log('几何体',geometry);
console.log('顶点位置数据',geometry.attributes.position);
console.log('顶点索引数据',geometry.index);

材质属性.wireframe

const material = new THREE.MeshLambertMaterial({
    color: 0x00ffff, 
    wireframe:true,//线条模式渲染mesh对应的三角形数据
});

几何体细分数

Three.js很多几何体都提供了细分数相关的参数,这里以矩形平面几何体PlaneGeometry为例介绍。

 //矩形几何体PlaneGeometry的参数3,4表示细分数,默认是1,1
const geometry = new THREE.PlaneGeometry(100,50,1,1);

const geometry = new THREE.PlaneGeometry(100,50,2,1); //4个三角形

const geometry = new THREE.PlaneGeometry(100,50,2,2); //8个三角形

球体SphereGeometry细分数

const geometry = new THREE.SphereGeometry( 50, 32, 16 );  //默认分割数32,16

// 如果球体细分数比较低,表面就不会那么光滑。
const geometry = new THREE.SphereGeometry( 15, 8, 8 );

三角形数量与性能

对于一个曲面而言,细分数越大,表面越光滑,但是三角形和顶点数量却越多。

几何体三角形数量或者说顶点数量直接影响Three.js的渲染性能,在不影响渲染效果的情况下,一般尽量越少越好。

5. 旋转、缩放、平移几何体

image.png

// 几何体xyz三个方向都放大2倍
geometry.scale(2, 2, 2);
// 几何体沿着x轴平移50
geometry.translate(50, 0, 0);
// 几何体绕着x轴旋转45度
geometry.rotateX(Math.PI / 4);
// 几何体旋转、缩放或平移之后,查看几何体顶点位置坐标的变化
// BufferGeometry的旋转、缩放、平移等方法本质上就是改变顶点的位置坐标
console.log('顶点位置数据', geometry.attributes.position);

模型对象、材质

1. 三维向量Vector3

//向量Vector3对象表示方向
const axis = new THREE.Vector3(2, 3, 4);
axis.normalize(); //向量归一化
//沿着axis轴表示方向平移100
mesh.translateOnAxis(axis, 100);

 2. 欧拉Euler与角度属性.rotation

模型的角度属性.rotation和四元数属性.quaternion都是表示模型的角度状态,只是表示方法不同,.rotation属性值是欧拉对象Euler,.quaternion属性值是是四元数对象Quaternion。

// 创建一个欧拉对象,表示绕着xyz轴分别旋转45度,0度,90度
const Euler = new THREE.Euler( Math.PI/4,0, Math.PI/2);

//绕y轴的角度设置为60度
mesh.rotation.y += Math.PI/3;
mesh.rotateY(Math.PI/3)

3. 模型材质颜色(Color对象)

查看颜色对象Color文档,可以看到颜色对象有三个属性,分别为.r.g.b,表示颜色RGB的三个分量。

// 查看Color对象设置0x00ff00对应的的.r、.g、.b值
const color = new THREE.Color(0x00ff00);
// 通过`.r`、`.g`、`.b`属性改变颜色值
color.r = 0.0;
color.b = 0.0;
//color提供了.setHex()、.setRGB()、.setStyle()、.set()方法
color.setRGB(0,1,0);//RGB方式设置颜色
color.setHex(0x00ff00);//十六进制方式设置颜色
color.setStyle('#00ff00');//前端CSS颜色值设置颜色
color.set(0x00ff00);//十六进制方式设置颜色
color.set('#00ff00');//前端CSS颜色值设置颜色
material.color.set('rgb(0,255,0)');  //rgb形式

4. 模型材质

材质共享

const mesh = new THREE.Mesh(geometry, material);
const mesh2 = new THREE.Mesh(geometry, material);
mesh2.position.x = 100;
// 两个mesh共享一个材质,改变一个mesh的颜色,另一个mesh2的颜色也会跟着改变
// mesh.material和mesh2.material都指向同一个material
// 三者等价:mesh.material、mesh2.material、material
mesh.material.color.set(0xffff00);
// 三者等价:mesh.geometry、mesh2.geometry、geometry
mesh.geometry.translate(0,100,0);

克隆.clone()和复制.copy()

克隆.clone()简单说就是复制一个和原对象一样的新对象,下面以三维向量对象Vector3给大家举例,其他的threejs对象都可以参照类似的写法。

const v1 = new THREE.Vector3(1, 2, 3);
console.log('v1',v1);
//v2是一个新的Vector3对象,和v1的.x、.y、.z属性值一样
const v2 = v1.clone();
console.log('v2',v2);

复制.copy()简单说就是把一个对象属性的属性值赋值给另一个对象,下面以三维向量对象Vector3给大家举例,其他的threejs对象都可以参照类似的写法。

const v1 = new THREE.Vector3(1, 2, 3);
const v3 = new THREE.Vector3(4, 5, 6);
//读取v1.x、v1.y、v1.z的赋值给v3.x、v3.y、v3.z
v3.copy(v1);

Mesh克隆.clone()

通过mesh克隆.clone()一个和mesh一样的新模型对象mesh2。通过克隆.clone()获得的新模型和原来的模型共享材质和几何体

const mesh2 = mesh.clone();
mesh2.position.x = 100;

// 改变材质颜色,或者说改变mesh2颜色,mesh和mesh2颜色都会改变
// material.color.set(0xffff00);
mesh2.material.color.set(0xffff00);

几何体和材质克隆.clone()

const mesh2 = mesh.clone();
// 克隆几何体和材质,重新设置mesh2的材质和几何体属性
mesh2.geometry = mesh.geometry.clone();
mesh2.material = mesh.material.clone();
// 改变mesh2颜色,不会改变mesh的颜色
mesh2.material.color.set(0xff0000);

mesh.position.copy()mesh.rotation.copy()

改变mesh的位置,使之位于mesh2的正上方(y),距离100。

mesh.position.copy(mesh2.position);//1. 第1步位置重合
mesh.position.y += 100;//1. 第2步mesh在原来y的基础上增加100

两个模型的姿态角度始终保持一样。

// 渲染循环
function render() {
    mesh.rotateY(0.01);// mesh旋转动画
    // 同步mesh2和mesh的姿态角度一样,不管mesh姿态角度怎么变化,mesh2始终保持同步
    mesh2.rotation.copy(mesh.rotation);
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}
render();

层级模型

1. Group层级模型(树结构)案例

场景对象scene构成的层级模型本身是一个树结构,场景对象层级模型的第一层,也就是树结构的根节点,一般来说网格模型Mesh、点模型Points、线模型Line是树结构的最外层叶子结点。构建层级模型的中间层一般都是通过Threejs的Group类来完成,Group类实例化的对象可以称为组对象。

image.png

场景对象Scene、组对象Group.add()方法都是继承自它们共同的基类(父类)Object3D.add()方法可以单独插入一个对象,也可以同时插入多个子对象。

父对象旋转缩放平移变换,子对象跟着变化

网格模型mesh1、mesh2作为设置为父对象group的子对象,如果父对象group进行旋转、缩放、平移变换,子对象同样跟着变换,就像你的头旋转了,眼睛会跟着头旋转。

//沿着Y轴平移mesh1和mesh2的父对象,mesh1和mesh2跟着平移
group.translateY(100);

//父对象缩放,子对象跟着缩放
group.scale.set(4,4,4);

//父对象旋转,子对象跟着旋转
group.rotateY(Math.PI/6)

mesh也能添加mesh子对象

threejs默认mesh也可以添加子对象,其实原因很简单,mesh和Group父类都是Object3D,本质上也可以认为都是Object3D。

//threejs默认mesh也可以添加子对象,mesh基类也是Object3D
mesh1.add(mesh2);

2. 历模型树结构、查询模型节点

模型命名(.name属性)

在层级模型中可以给一些模型对象通过.name属性命名进行标记。

const group = new THREE.Group();
group.name='小区房子';
const mesh = new THREE.Mesh(geometry, material);
mesh.name='一号楼';

递归遍历方法.traverse()

Threejs层级模型就是一个树结构,可以通过递归遍历的算法去遍历Threejs一个模型对象包含的所有后代。

// 递归遍历model包含所有的模型节点
model.traverse(function(obj) {
    console.log('所有模型节点的名称',obj.name);
    // obj.isMesh:if判断模型对象obj是不是网格模型'Mesh'
    if (obj.isMesh) {//判断条件也可以是obj.type === 'Mesh'
        obj.material.color.set(0xffff00);
    }
});

查找某个具体的模型.getObjectByName()

// 返回名.name为"4号楼"对应的对象
const nameNode = scene.getObjectByName ("4号楼");
nameNode.material.color.set(0xff0000);

3. 地坐标和世界坐标

本地(局部)坐标和世界坐标

// mesh的世界坐标就是mesh.position与group.position的累加
const mesh = new THREE.Mesh(geometry, material); 
mesh.position.set(50, 0, 0);
const group = new THREE.Group();
group.add(mesh);
group.position.set(50, 0, 0);
  1. 改变子对象的.position,子对象在3D空间中的坐标会发生改变。
  2. 改变父对象的.position,子对象在3D空间中的位置也会跟着变化,也就是说父对象.position和子对象.position叠加才是才是子对象的.position

任何一个模型的本地坐标(局部坐标)就是模型的.position属性。

一个模型的世界坐标,说的是,模型自身.position和所有父对象.position累加的坐标。

.getWorldPosition()获取世界坐标

// 声明一个三维向量用来表示某个坐标
const worldPosition = new THREE.Vector3();
// 获取mesh的世界坐标,你会发现mesh的世界坐标受到父对象group的.position影响
mesh.getWorldPosition(worldPosition);
console.log('世界坐标',worldPosition);
console.log('本地坐标',mesh.position);

4. 移除对象remove()

场景对象Scene、组对象Group、网格模型对象Mesh.remove()方法都是继承自它们共同的基类(父类)Object3D

.remove()方法使用

// 删除父对象group的子对象网格模型mesh1
group.remove(mesh1);

scene.remove(ambient);//移除场景中环境光

5. 模型显示与隐藏

开发web3d项目,有时候需要临时隐藏一个模型,或者一个模型处于隐藏状态,需要重新恢复显示。

模型属性.visible

模型对象的父类Object3D封装了一个属性.visible,通过该属性可以隐藏或显示一个模型

mesh.visible =false;// 隐藏一个网格模型,visible的默认值是true
group.visible =false;// 隐藏一个包含多个模型的组对象group
mesh.visible =true;// 使网格模型mesh处于显示状态

材质属性.visible

材质对象的父类Material封装了一个.visible属性,通过该属性可以控制是否隐藏该材质对应的模型对象。

// 隐藏网格模型mesh,visible的默认值是true
mesh.material.visible =false;
// 注意如果mesh2和mesh的.material属性指向同一个材质,mesh2也会跟着mesh隐藏

注意:如果多个模型引用了同一个材质,如果该材质.visible设置为false,意味着隐藏绑定该材质的所有模型。

顶点UV坐标、纹理贴图

1. 创建纹理贴图

通过纹理贴图加载器TextureLoaderload()方法加载一张图片可以返回一个纹理对象Texture,纹理对象Texture可以作为模型材质颜色贴图.map属性的值。

//长方形
const geometry = new THREE.PlaneGeometry(200, 100); 
//纹理贴图加载器TextureLoader
const texLoader = new THREE.TextureLoader();
// .load()方法加载图像,返回一个纹理对象Texture
const texture = texLoader.load('./earth.jpg');
const material = new THREE.MeshLambertMaterial({
    // 设置纹理贴图:Texture对象作为材质map属性的属性值
    map: texture,//map表示材质的颜色贴图属性
});

//CircleGeometry的顶点UV坐标是按照圆形采样纹理贴图
const geometry = new THREE.CircleGeometry(60, 100);
//纹理贴图加载器TextureLoader
const texLoader = new THREE.TextureLoader();
const texture = texLoader.load('./texture.jpg');
const material = new THREE.MeshBasicMaterial({
    map: texture,//map表示材质的颜色贴图属性
    side:THREE.DoubleSide,
});
const mesh = new THREE.Mesh(geometry, material);

2. 定义顶点UV坐标

顶点UV坐标的作用

顶点UV坐标的作用是从纹理贴图上提取像素映射到网格模型Mesh的几何体表面上。

浏览器控制台查看threejs几何体默认的UV坐标数据。

const geometry = new THREE.PlaneGeometry(200, 100); //矩形平面
// const geometry = new THREE.BoxGeometry(100, 100, 100); //长方体
// const geometry = new THREE.SphereGeometry(100, 30, 30);//球体
console.log('uv',geometry.attributes.uv);

纹理贴图UV坐标范围

顶点UV坐标可以在0~1.0之间任意取值,纹理贴图左下角对应的UV坐标是(0,0)右上角对应的坐标(1,1)

image.png

自定义顶点UVgeometry.attributes.uv

顶点UV坐标geometry.attributes.uv和顶点位置坐标geometry.attributes.position是一一对应的,

UV顶点坐标你可以根据需要在0~1之间任意设置,具体怎么设置,要看你想把图片的哪部分映射到Mesh的几何体表面上。

/**纹理坐标0~1之间随意定义*/
const uvs = new Float32Array([
    0, 0, //图片左下角
    1, 0, //图片右下角
    1, 1, //图片右上角
    0, 1, //图片左上角
]);
// 获取纹理贴图左下角四分之一部分的像素值
const uvs = new Float32Array([
    0, 0, 
    0.5, 0, 
    0.5, 0.5, 
    0, 0.5, 
]);
// 设置几何体attributes属性的位置normal属性
geometry.attributes.uv = new THREE.BufferAttribute(uvs, 2); //2个为一组,表示一个顶点的纹理坐标

3. 纹理对象Texture阵列

使用threejs纹理对象Texture的阵列功能+矩形平面几何体PlaneGeometry实现一个地面瓷砖效果。

const geometry = new THREE.PlaneGeometry(2000, 2000);
//纹理贴图加载器TextureLoader
const texLoader = new THREE.TextureLoader();
// .load()方法加载图像,返回一个纹理对象Texture
const texture = texLoader.load('./瓷砖.jpg');

// 设置阵列模式
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
// uv两个方向纹理重复数量
texture.repeat.set(12,12);//注意选择合适的阵列数量

const material = new THREE.MeshLambertMaterial({
    // 设置纹理贴图:Texture对象作为材质map属性的属性值
    map: texture,//map表示材质的颜色贴图属性
});
const mesh = new THREE.Mesh(geometry, material);

如果背景为透明png贴图,要打开透明设置

const material = new THREE.MeshBasicMaterial({
    map: textureLoader.load('./指南针.png'),        
    transparent: true, //使用背景透明的png贴图,注意开启透明计算
});

4. UV动画

通过纹理对象的偏移属性.offset实现一个UV动画效果。

纹理对象Texture的.offset的功能是偏移贴图在Mesh上位置,本质上相当于修改了UV顶点坐标

texture.offset.x +=0.5;//纹理U方向偏移
texture.offset.y +=0.5;//纹理V方向偏移

image.png

纹理贴图阵列 + UV动画

// 设置U方向阵列模式
texture.wrapS = THREE.RepeatWrapping;
// uv两个方向纹理重复数量
texture.repeat.x=50;//注意选择合适的阵列数量

// 渲染循环
function render() {
    texture.offset.x +=0.1;//设置纹理动画:偏移量根据纹理和动画需要,设置合适的值
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}
render();

渲染器和前端UI界面

1. Three.js渲染结果保存为图片

配置webgl渲染器preserveDrawingBuffer:true

// WebGL渲染器设置
const renderer = new THREE.WebGLRenderer({
    //想把canvas画布上内容下载到本地,需要设置为true
    preserveDrawingBuffer:true,
});

按钮绑定鼠标事件

// 鼠标单击id为download的HTML元素,threejs渲染结果以图片形式下载到本地
document.getElementById('download').addEventListener('click',function(){
    
})

创建超链接元素a:用于保存下载文件

// 鼠标单击id为download的HTML元素,threejs渲染结果以图片形式下载到本地
document.getElementById('download').addEventListener('click',function(){
    // 创建一个超链接元素,用来下载保存数据的文件
    const link = document.createElement('a');
    // 通过超链接herf属性,设置要保存到文件中的数据
    link.href = ;
    link.download = 'threejs.png'; //下载文件名
    link.click(); //js代码触发超链接元素a的鼠标点击事件,开始下载文件到本地
})

Cavnas方法.toDataURL()

Canvas画布通过.toDataURL()方法可以获取画布上的像素信息。canvas.toDataURL("image/png");表示以png格式获取像素数据,可以直接赋值给超链接元素a的.herf属性下载到本地。

const link = document.createElement('a');
// 通过超链接herf属性,设置要保存到文件中的数据
const canvas = renderer.domElement; //获取canvas对象
link.href = canvas.toDataURL("image/png");

2. 深度冲突(模型闪烁)

对于模型闪烁的原因简单地说就是深度冲突,对应的英文关键词是Z-fighting。 模型闪烁主要是两个Mesh重合,电脑GPU分不清谁在前谁在后,这种现象,可以称为深度冲突Z-fighting

解决方法:

  • 两个矩形Mesh拉开距离
  • 透视投影相机对距离影响
  • webgl渲染器设置对数深度缓冲区
// WebGL渲染器设置
const renderer = new THREE.WebGLRenderer({
    // 设置对数深度缓冲区,优化深度冲突问题
    logarithmicDepthBuffer: true
});

生成曲线和几何体

1. 生成圆弧顶点

const geometry = new THREE.BufferGeometry(); //创建一个几何体对象
const R = 100; //圆弧半径
const N = 50; //分段数量
const sp = 2 * Math.PI / N;//两个相邻点间隔弧度
// 批量生成圆弧上的顶点数据
const arr = [];
for (let i = 0; i < N; i++) {
    const angle =  sp * i;//当前点弧度
    // 以坐标原点为中心,在XOY平面上生成圆弧上的顶点数据
    const x = R * Math.cos(angle);
    const y = R * Math.sin(angle);
    arr.push(x, y, 0);
}
//类型数组创建顶点数据
const vertices = new Float32Array(arr);
// 创建属性缓冲区对象
//3个为一组,表示一个顶点的xyz坐标
const attribue = new THREE.BufferAttribute(vertices, 3); 
// 设置几何体attributes属性的位置属性
geometry.attributes.position = attribue;

// 线材质
const material = new THREE.LineBasicMaterial({
    color: 0xff0000 //线条颜色
});
// 创建线模型对象   构造函数:Line、LineLoop、LineSegments
// const line = new THREE.Line(geometry, material); 
const line = new THREE.LineLoop(geometry, material);//线条模型对象

2. 几何体方法.setFromPoints()

几何体BufferGeometry的一个方法.setFromPoints().setFromPoints()是几何体BufferGeometry的一个方法,通过该方法可以把数组pointsArr中坐标数据提取出来赋值给几何体。具体说就是把pointsArr里面坐标数据提取出来,赋值给geometry.attributes.position属性

const pointsArr = [
    // 三维向量Vector3表示的坐标值
    new THREE.Vector3(0,0,0),
    new THREE.Vector3(0,100,0),
    new THREE.Vector3(0,100,100),
    new THREE.Vector3(0,0,100),
];

// 把数组pointsArr里面的坐标数据提取出来,赋值给`geometry.attributes.position`属性
geometry.setFromPoints(pointsArr);
console.log('几何体变化',geometry.attributes.position);

3. 曲线Curve简介

threejs提供了很多常用的曲线或直线API,可以直接使用。这些API曲线都有一个共同的父类Curve

image.png

椭圆、圆

// 椭圆弧线EllipseCurve
EllipseCurve( aX, aY, xRadius,yRadius, aStartAngle, aEndAngle, aClockwise )

image.png

// 圆弧线ArcCurve
ArcCurve( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise )

image.png

曲线精度

//曲线上取点,参数表示取点细分精度
const pointsArr = arc.getPoints(50); //分段数50,返回51个顶点
// const pointsArr = arc.getPoints(10);//取点数比较少,圆弧线不那么光滑

4. 样条曲线

对于一些不规则的曲线,很难用一个圆、椭圆或抛物线函数去描述,这时候,可以使用threejs提供的样条曲线贝塞尔曲线去表达。

image.png

三维样条曲线CatmullRomCurve3

在三维空间中随意设置几个顶点坐标,然后作为三维样条曲线CatmullRomCurve3的参数,你就可以生成一条穿过这几个点的光滑曲线。

// 三维向量Vector3创建一组顶点坐标
const arr = [
    new THREE.Vector3(-50, 20, 90),
    new THREE.Vector3(-10, 40, 40),
    new THREE.Vector3(0, 0, 0),
    new THREE.Vector3(60, -60, 0),
    new THREE.Vector3(70, 0, 80)
]
// 三维样条曲线
const curve = new THREE.CatmullRomCurve3(arr);

//曲线上获取点
const pointsArr = curve.getPoints(100); 
const geometry = new THREE.BufferGeometry();
//读取坐标数据赋值给几何体顶点
geometry.setFromPoints(pointsArr); 
// 线材质
const material = new THREE.LineBasicMaterial({
    color: 0x00fffff
});
// 线模型
const line = new THREE.Line(geometry, material);

// 用点模型可视化样条曲线经过的顶点位置
const geometry2 = new THREE.BufferGeometry();
geometry2.setFromPoints(arr);
const material2 = new THREE.PointsMaterial({
    color: 0xff00ff,
    size: 10,
});
//点模型对象
const points = new THREE.Points(geometry2, material2);

二维样条曲线

二维样条曲线SplineCurve默认情况下就是在XOY平面生成一个平面的样条曲线。

// 二维向量Vector2创建一组顶点坐标
const arr = [
    new THREE.Vector2(-100, 0),
    new THREE.Vector2(0, 30),
    new THREE.Vector2(100, 0),
];
// 二维样条曲线
const curve = new THREE.SplineCurve(arr);

5. 贝塞尔曲线

二维二次贝塞尔曲线QuadraticBezierCurve

// p1、p2、p3表示三个点坐标
// p1、p3是曲线起始点,p2是曲线的控制点
const p1 = new THREE.Vector2(-80, 0);
const p2 = new THREE.Vector2(20, 100);
const p3 = new THREE.Vector2(80, 0);

// 二维二次贝赛尔曲线
const curve = new THREE.QuadraticBezierCurve(p1, p2, p3);

const pointsArr = curve.getPoints(100); //曲线上获取点
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(pointsArr); //读取坐标数据赋值给几何体顶点
const material = new THREE.LineBasicMaterial({color: 0x00fffff});
const line = new THREE.Line(geometry, material);

三维二次贝赛尔曲线QuadraticBezierCurve3

三维二次贝赛尔曲线QuadraticBezierCurve3与二维二次贝赛尔曲线QuadraticBezierCurve区别就是多了一个维度,参数是三维向量对象Vector3。

// p1、p2、p3表示三个点坐标
const p1 = new THREE.Vector3(-80, 0, 0);
const p2 = new THREE.Vector3(20, 100, 0);
const p3 = new THREE.Vector3(80, 0, 100);
// 三维二次贝赛尔曲线
const curve = new THREE.QuadraticBezierCurve3(p1, p2, p3);

二维三次贝塞尔曲线CubicBezierCurve

二维三次贝塞尔曲线CubicBezierCurve与二维二次贝赛尔曲线QuadraticBezierCurve区别就是多了一个控制点。

// p1、p2、p3、p4表示4个点坐标
// p1、p4是曲线起始点,p2、p3是曲线的控制点
const p1 = new THREE.Vector2(-80, 0);
const p2 = new THREE.Vector2(-40, 50);
const p3 = new THREE.Vector2(50, 50);
const p4 = new THREE.Vector2(80, 0);

// 二维三次贝赛尔曲线
const curve = new THREE.CubicBezierCurve(p1, p2, p3, p4);

三维三次贝赛尔曲线CubicBezierCurve3

三维三次贝赛尔曲线CubicBezierCurve3与二维三次贝塞尔曲线CubicBezierCurve区别就是多了一个维度,参数是三维向量对象Vector3。

const p1 = new THREE.Vector3(-80, 0, 0);
const p2 = new THREE.Vector3(-40, 50, 0);
const p3 = new THREE.Vector3(50, 50, 0);
const p4 = new THREE.Vector3(80, 0, 100);
// 三维三次贝赛尔曲线
const curve = new THREE.CubicBezierCurve3(p1, p2, p3, p4);

6. 样条、贝塞尔曲线应用(*)

曲线API在大屏可视化中的应用:地图大屏可视化、地球大屏可视化。表示一个飞线曲线轨迹有多重方案,圆弧、椭圆弧、贝塞尔、样条...

三维样条曲线CatmullRomCurve3实现飞线轨迹

// p1、p3轨迹线起始点坐标
const p1 = new THREE.Vector3(-100, 0, -100);
const p3 = new THREE.Vector3(100, 0, 100);
// 计算p1和p3的中点坐标
const x2 = (p1.x + p3.x)/2;
const z2 = (p1.z + p3.z)/2;
const h = 50;
const p2 = new THREE.Vector3(x2, h, z2);

const arr = [p1, p2, p3];
// 三维样条曲线
const curve = new THREE.CatmullRomCurve3(arr);

三维二次贝赛尔曲线QuadraticBezierCurve3实现飞线轨迹

// p1、p3轨迹线起始点坐标
const p1 = new THREE.Vector3(-100, 0, -100);
const p3 = new THREE.Vector3(100, 0, 100);
// 计算p1和p3的中点坐标
const x2 = (p1.x + p3.x)/2;
const z2 = (p1.z + p3.z)/2;
const h = 100;
const p2 = new THREE.Vector3(x2, h, z2);
// 三维二次贝赛尔曲线
const curve = new THREE.QuadraticBezierCurve3(p1, p2, p3);

7. 组合曲线CurvePath拼接曲线(*)

通过threejs组合曲线CurvePath对象,你可以把直线、圆弧、贝塞尔等线条拼接为一条曲线。

const R = 80;//圆弧半径
const H = 200;//直线部分高度
// 直线1
const line1 = new THREE.LineCurve(new THREE.Vector2(R, H), new THREE.Vector2(R, 0));
// 圆弧
const arc = new THREE.ArcCurve(0, 0, R, 0, Math.PI, true);
// 直线2
const line2 = new THREE.LineCurve(new THREE.Vector2(-R, 0), new THREE.Vector2(-R, H));

// CurvePath创建一个组合曲线对象
const CurvePath = new THREE.CurvePath();
//line1, arc, line2拼接出来一个U型轮廓曲线,注意顺序
CurvePath.curves.push(line1, arc, line2);

注意:曲线首尾相接

有一点要注意,组合曲线的坐标顺序和线条组合顺序不能随意写,总的方向,就是先确定整个曲线的起点,然后沿着一个方向依次绘制不同曲线,确保不同曲线首尾相接。

  • 直线的起点是直线的第一个参数
  • 圆弧线的起点,默认就是从x轴正半轴开始

image.png

组合曲线CurvePath取点

组合曲线CurvePath和它的父类Curve一样具有.getPoints().getSpacedPoints()取点方法。

//组合曲线上获取点
const pointsArr = CurvePath.getPoints(16);   //方法1
const pointsArr = CurvePath.getSpacedPoints(16); //方法2

const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(pointsArr); //读取坐标数据赋值给几何体顶点

8. 曲线路径管道TubeGeometry(*)

管道TubeGeometry几何体的功能就是基于一个3D曲线路径,生成一个管道几何体。

构造函数格式:TubeGeometry(path, tubularSegments, radius, radiusSegments, closed)

image.png

CurvePath多段路径生成管道案例(*)

CurvePath组合曲线,也可以作为TubeGeometry的参数1,用于生成管道几何体。

下面组合曲线CurvePath是由一段三维贝塞尔曲线QuadraticBezierCurve3加上两段3D直线LineCurve3拼接组成。

// 创建多段线条的顶点数据
const p1 = new THREE.Vector3(0, 0,100)
const p2 = new THREE.Vector3(0, 0,30);
const p3 = new THREE.Vector3(0, 0,0);
const p4 = new THREE.Vector3(30, 0, 0);
const p5 = new THREE.Vector3(100, 0, 0);
// 1. 3D直线线段
const line1 = new THREE.LineCurve3(p1, p2);
// 2. 三维二次贝塞尔曲线
const curve = new THREE.QuadraticBezierCurve3(p2, p3, p4);
// 3. 3D直线线段
const line2 = new THREE.LineCurve3(p4, p5);

const CurvePath = new THREE.CurvePath(); 
// 三条线拼接为一条曲线
CurvePath.curves.push(line1, curve, line2); 

// CurvePath:路径   40:沿着轨迹细分数  2:管道半径   25:管道截面圆细分数
const geometry = new THREE.TubeGeometry(CurvePath, 50, 2, 25);

9. 旋转成型LatheGeometry

生活中有很多的几何体具备旋转特征, three.js提供了一个类LatheGeometry(), LatheGeometry可以利用一个2D轮廓,经过旋转变换生成一个3D的几何体曲面。

image.png

格式:LatheGeometry(points, segments, phiStart, phiLength)

image.png

通过二维样条曲线SplineCurve生成一个光滑的曲线旋转轮廓。

// 通过三个点定义一个二维样条曲线
const curve = new THREE.SplineCurve([
    new THREE.Vector2(50, 60),
    new THREE.Vector2(25, 0),
    new THREE.Vector2(50, -60)
]);
//曲线上获取点,作为旋转几何体的旋转轮廓
const pointsArr = curve.getPoints(50); 
console.log('旋转轮廓数据',pointsArr);
// LatheGeometry:pointsArr轮廓绕y轴旋转生成几何体曲面
const geometry = new THREE.LatheGeometry(pointsArr, 30);

10. 轮廓填充ShapeGeometry(*)

有些时候已知一个多边形的外轮廓坐标,想通过这些外轮廓坐标生成一个多边形几何体平面,这时候你可以借助threejs提供的轮廓填充ShapeGeometry几何体实现。

// 一组二维向量表示一个多边形轮廓坐标
const pointsArr = [
    new THREE.Vector2(-50, -50),
    new THREE.Vector2(-60, 0),
    new THREE.Vector2(0, 50),
    new THREE.Vector2(60, 0),
    new THREE.Vector2(50, -50),
]
// Shape表示一个平面多边形轮廓,参数是二维向量构成的数组pointsArr
const shape = new THREE.Shape(pointsArr);
const geometry = new THREE.ShapeGeometry(shape);

11. 拉伸ExtrudeGeometry(*)

拉伸几何体ExtrudeGeometry和上节课讲到的轮廓填充几何体ShapeGeometry一样,都是基于一个基础的平面轮廓Shape进行变换,生成一个几何体。

image.png

// Shape表示一个平面多边形轮廓
const shape = new THREE.Shape([
    // 按照特定顺序,依次书写多边形顶点坐标
    new THREE.Vector2(-50, -50), //多边形起点
    new THREE.Vector2(-50, 50),
    new THREE.Vector2(50, 50),
    new THREE.Vector2(50, -50),
]);

const geometry = new THREE.ExtrudeGeometry(
    shape,{
        depth: 20,
    }
);

12. 扫描ExtrudeGeometry

通过ExtrudeGeometry除了可以实现拉伸成型,也可以让一个平面轮廓Shape沿着曲线扫描成型。

image.png

// 扫描轮廓:Shape表示一个平面多边形轮廓
const shape = new THREE.Shape([
    // 按照特定顺序,依次书写多边形顶点坐标
    new THREE.Vector2(0,0), //多边形起点
    new THREE.Vector2(0,10),
    new THREE.Vector2(10,10),
    new THREE.Vector2(10,0),
]);

// 扫描轨迹:创建轮廓的扫描轨迹(3D样条曲线)
const curve = new THREE.CatmullRomCurve3([
    new THREE.Vector3( -10, -50, -50 ),
    new THREE.Vector3( 10, 0, 0 ),
    new THREE.Vector3( 8, 50, 50 ),
    new THREE.Vector3( -5, 0, 100)
]);

//扫描造型:扫描默认没有倒角
const geometry = new THREE.ExtrudeGeometry(
    shape, //扫描轮廓
    {
        extrudePath:curve,//扫描轨迹
        steps:100//沿着路径细分精度,越大越光滑
    }
);

13. 多边形轮廓Shape简介(*)

上节课提到多边形轮廓Shape,是直接通过一组二维向量Vector2表示的xy点坐标创建。下面是通过Shape的一些2D绘图API表达多边形轮廓。

.currentPoint属性

.currentPoint属性字面意思是当前点,默认值Vector2(0,0)

实例化一个ShapePath对象,查看.currentPoint属性的默认值。

const shape = new THREE.Shape();
const path = new THREE.Path();
console.log('currentPoint',shape.currentPoint);

.moveTo()方法

执行和.moveTo()方法查看.currentPoint属性变化。

const shape = new THREE.Shape();
shape.moveTo(10,0);
console.log('currentPoint',shape.currentPoint);

除了.moveTo()方法,Path其他的直线、圆弧等方法也可能会改变.currentPoint属性

绘制直线.lineTo()

.lineTo()绘制直线线段,直线线段的起点是当前点属性.currentPoint表示的位置,结束点是.lineTo()的参数表示的坐标。

const shape = new THREE.Shape();
shape.moveTo(10,0);//.currentPoint变为(10,0)
// 绘制直线线段,起点(10,0),结束点(100,0)
shape.lineTo(100,0);

.lineTo()方法和.moveTo()方法,一样会改变.currentPoint属性

shape.lineTo(100,0);//.currentPoint变为(100,0)
console.log('currentPoint',shape.currentPoint);

绘制一个矩形轮廓Shape

const shape = new THREE.Shape();
shape.moveTo(10, 0); //.currentPoint变为(10,0)
// 绘制直线线段,起点(10,0),结束点(100,0)
shape.lineTo(100, 0);//.currentPoint变为(100, 0)
shape.lineTo(100, 100);//.currentPoint变为(100, 100)
shape.lineTo(10, 100);//.currentPoint变为(10, 100)

创建好的多边形轮廓Shape作为几何体的参数

// ShapeGeometry填充Shape获得一个平面几何体
const geometry = new THREE.ShapeGeometry(shape);
// ExtrudeGeometry拉伸Shape获得一个长方体几何体
const geometry = new THREE.ExtrudeGeometry(shape, {
    depth:20,//拉伸长度
    bevelEnabled:false,//禁止倒角
});

14. 几何体顶点颜色数数据

下面给大家介绍顶点颜色.attributes.color数据。

  • 顶点位置数据geometry.attributes.position
  • 顶点法向量数据geometry.attributes.normal
  • 顶点UV数据geometry.attributes.uv
  • 顶点颜色数据geometry.attributes.color

#几何体顶点颜色.attributes.color

几何体BufferGeometry顶点位置数据.attributes.position

const geometry = new THREE.BufferGeometry(); //创建一个几何体对象
const vertices = new Float32Array([
    0, 0, 0, //顶点1坐标
    50, 0, 0, //顶点2坐标
    0, 25, 0, //顶点3坐标
]);
// 顶点位置
geometry.attributes.position = new THREE.BufferAttribute(vertices, 3);

与几何体BufferGeometry顶点位置数据.attributes.position一一对应的顶点颜色数据.attributes.color

每个点对应一个位置数据,同时对应一个颜色数据。

const colors = new Float32Array([
    1, 0, 0, //顶点1颜色
    0, 0, 1, //顶点2颜色
    0, 1, 0, //顶点3颜色
]);
// 设置几何体attributes属性的颜色color属性
//3个为一组,表示一个顶点的颜色数据RGB
geometry.attributes.color = new THREE.BufferAttribute(colors, 3); 

点模型Points渲染顶点颜色数据

通过点、线、网格模型渲染几何体Geometry,如果希望顶点颜色.attributes.color起作用,需要设置材质属性vertexColors:true,下面以以点模型为例,可以看到geometry的不同点被你设置为了不同颜色。

// 点渲染模式
const material = new THREE.PointsMaterial({
    // color: 0x333333,//使用顶点颜色数据,color属性可以不用设置
    vertexColors:true,//默认false,设置为true表示使用顶点颜色渲染
    size: 20.0, //点对象像素尺寸
});
const points = new THREE.Points(geometry, material); //点模型对象

颜色渐变(颜色插值)

自定几何体顶点颜色数据,然后用线模型Line渲染,你可以看到直线的颜色是渐变的。

下面代码两端直线,分别是红色到蓝色渐变、蓝色到绿色渐变。

const colors = new Float32Array([
    1, 0, 0, //顶点1颜色 
    0, 0, 1, //顶点2颜色
    0, 1, 0, //顶点3颜色
]);
geometry.attributes.color = new THREE.BufferAttribute(colors, 3); 


const material = new THREE.LineBasicMaterial({
    vertexColors:true,//使用顶点颜色渲染
});
const line = new THREE.Line(geometry, material);

几何体顶点颜色.attributes.color设置的直线颜色渐变效果

网格模型颜色渐变

自定几何体顶点颜色数据,然后用网格模型Mesh渲染,和Line一样,也会产生颜色渐变效果。

const material = new THREE.MeshBasicMaterial({
    // color: 0x333333,//使用顶点颜色数据,color属性可以不用设置
    vertexColors:true,//默认false,设置为true表示使用顶点颜色渲染
    side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(geometry, material); 

15. 一段曲线颜色渐变(*)

可以通过几何体顶点颜色.attributes.color数据,实现一段曲线颜色渐变效果。

const geometry = new THREE.BufferGeometry(); //创建一个几何体对象
// 三维样条曲线
const curve = new THREE.CatmullRomCurve3([
    new THREE.Vector3(-50, 20, 90),
    new THREE.Vector3(-10, 40, 40),
    new THREE.Vector3(0, 0, 0),
    new THREE.Vector3(60, -60, 0),
    new THREE.Vector3(70, 0, 80)
]);
const pointsArr = curve.getSpacedPoints(100); //曲线取点      
geometry.setFromPoints(pointsArr); //pointsArr赋值给顶点位置属性 

const pos = geometry.attributes.position;
const count = pos.count; //顶点数量
// 计算每个顶点的颜色值
const colorsArr = [];
for (let i = 0; i < count; i++) {
    const percent = i / count; //点索引值相对所有点数量的百分比
    //根据顶点位置顺序大小设置颜色渐变
    // 红色分量从0到1变化,蓝色分量从1到0变化
    colorsArr.push(percent, 0, 1 - percent); //蓝色到红色渐变色
}
//类型数组创建顶点颜色color数据
const colors = new Float32Array(colorsArr);
// 设置几何体attributes属性的颜色color属性
geometry.attributes.color = new THREE.BufferAttribute(colors, 3);

const material = new THREE.LineBasicMaterial({
    vertexColors: true, //使用顶点颜色渲染
});
const line = new THREE.Line(geometry, material);

16. Color颜色渐变插值(*)

颜色对象Color颜色渐变插值方法.lerpColors().lerp()

颜色对象Color颜色插值方法.lerpColors()

执行.lerpColors(Color1,Color2, percent)通过一个百分比参数percent,可以控制Color1和Color2两种颜色混合的百分比,Color1对应1-percent,Color2对应percent

const c1 = new THREE.Color(0xff0000); //红色
const c2 = new THREE.Color(0x0000ff); //蓝色
const c = new THREE.Color();
c.lerpColors(c1,c2, 0);  //100% c1 + 0% c2混合
c.lerpColors(c1,c2, 0.5);  //c1和c2各取50%

颜色对象Color颜色插值方法.lerp()

.lerp().lerpColors()功能一样,只是具体语法细节不同。 c1与c2颜色混合,混合后的rgb值,赋值给c1的.r.g.b属性。

const c1 = new THREE.Color(0xff0000); //红色
const c2 = new THREE.Color(0x0000ff); //蓝色
c1.lerp(c2, percent);

相机基础

1.正投影相机

之前介绍了常用的透视投影相机PerspectiveCamera,下面介绍正投影相机OrthographicCamera

正投影相机的长方体可视化空间和透视投影PerspectiveCamera视锥体相似,只是形状不同。

image.png

// 构造函数格式
OrthographicCamera( left, right, top, bottom, near, far )

image.png

相机选择

对于大部分需要模拟人眼观察效果的场景,需要使用透视投影相机,比如人在场景中漫游,或是在高处俯瞰整个园区或工厂。

正投影没有透视效果,也就是不会模拟人眼观察世界的效果。在一些不需要透视的场景你可以选择使用正投影相机,比如整体预览一个中国地图的效果,或者一个2D可视化的效果。

2. 包围盒Box3(*)

所谓包围盒Box3,就是一个长方体空间,把模型的所有顶点数据包围在一个最小的长方体空间中,这个最小长方体空间就是该模型的包围盒Box3

image.png

计算模型最小包围盒.expandByObject()

模型对象,比如mesh或group,作为.expandByObject()的参数,可以计算该模型的包围盒。

const box3 = new THREE.Box3();
box3.expandByObject(mesh); // 计算模型包围盒
console.log('查看包围盒',box3);

浏览器控制台你可以通过.min.max属性查看模型的包围盒信息。

包围盒尺寸.getSize()

返回包围盒具体的长宽高尺寸

const scale = new THREE.Vector3()
// getSize()计算包围盒尺寸
// 获得包围盒长宽高尺寸,结果保存在参数三维向量对象scale中
box3.getSize(scale)
console.log('模型包围盒尺寸', scale);

包围盒几何中心.getCenter()

Box3方法.getCenter()计算返回包围盒几何中心

// 计算包围盒中心坐标
const center = new THREE.Vector3()
box3.getCenter(center)
console.log('模型中心坐标', center);

3. OrbitControls旋转缩放限制(*)

禁止右键平移.enablePan属性

controls.enablePan = false; //禁止右键拖拽

禁止缩放或旋转

controls.enableZoom = false;//禁止缩放
controls.enableRotate = false; //禁止旋转

OrbitControls.target属性

相机控件OrbitControls.target属性对应的就是相机的.lookAt()观察目标。

执行controls.update();,相机控件内部会执行camera.lookAt(controls.target)

// controls.target默认值是坐标原点
controls.target.set(x, y, z);
//update()函数内会执行camera.lookAt(x, y, z)
controls.update(); 

透视投影相机缩放范围

.minDistance表示相机位置.position和相机目标观察点controls.target的最小距离。

//相机位置与观察目标点最小值
controls.minDistance = 200;

.maxDistance表示相机位置.position和相机目标观察点controls.target的最大距离。

//相机位置与观察目标点最大值
controls.maxDistance = 500;

正投影缩放范围

// 缩放范围
controls.minZoom = 0.5;
controls.maxZoom = 2;

相机位置与目标观察点距离.getDistance()

//相机位置与目标观察点距离
const dis = controls.getDistance();
console.log('dis',dis);

设置旋转范围

// 上下旋转范围
controls.minPolarAngle = 0;//默认值0
controls.maxPolarAngle = Math.PI;//默认值Math.PI

.maxPolarAngle属性设置为90度,这样不能看到工厂模型底部

controls.maxPolarAngle = Math.PI/2;

通过.minAzimuthAngle.maxAzimuthAngle属性控制左右的旋转范围。

// 左右旋转范围
controls.minAzimuthAngle = -Math.PI/2;
controls.maxAzimuthAngle = Math.PI/2;

光源和阴影

image.png

gui辅助调节光源阴影(*)

阴影范围可视化调节

  • 根据工厂尺寸数量级预先设置.shadow.camera,然后通过GUI调试选择一个合适的值
  • .shadow.camera的位置通过光源的位置调试。
  • .shadow.camera参数改变后,注意执行cameraHelper.update();更新
// 阴影子菜单
const shadowFolder = gui.addFolder('平行光阴影');
const cam = directionalLight.shadow.camera;
// 相机left、right等属性变化执行.updateProjectionMatrix();
// 相机变化了,执行CameraHelper的更新方法.update();
shadowFolder.add(cam,'left',-500,0).onChange(function(v){
    cam.updateProjectionMatrix();//相机更新投影矩阵
    cameraHelper.update();//相机范围变化了,相机辅助对象更新
});

其他参数类似设置

shadowFolder.add(cam,'right',0,500).onChange(function(v){
    cam.updateProjectionMatrix();
    cameraHelper.update();
});
shadowFolder.add(cam,'top',0,500).onChange(function(v){
    cam.updateProjectionMatrix();
    cameraHelper.update();
});
shadowFolder.add(cam,'bottom',-500,0).onChange(function(v){
    cam.updateProjectionMatrix();
    cameraHelper.update();
});
shadowFolder.add(cam,'far',0,1000).onChange(function(v){
    cam.updateProjectionMatrix();
    cameraHelper.update();
});

精灵模型Sprite(*)

1.创建精灵图

Three.js的精灵模型Sprite和Threejs的网格模型Mesh一样都是模型对象,父类都是Object3D,关于精灵模型对象Sprite的方法和属性除了可以查看文档Sprite,也可以查看父类Object3D

image.png image.png

// 创建精灵材质对象SpriteMaterial
const spriteMaterial = new THREE.SpriteMaterial({
  color:0x00ffff,//设置颜色
});

// 创建精灵模型对象,不需要几何体geometry参数
const sprite = new THREE.Sprite(spriteMaterial);

const mesh = new THREE.Mesh(geometry, material);

精灵模型标注场景(贴图)

如果希望矩形始终平行于canvas画布,就选择Sprite,如果希望矩形标注姿态角度能跟着场景旋转,就使用矩形Mesh标注场景。

// 精灵模型设置颜色贴图
const texture = new THREE.TextureLoader().load("./光点.png");
const spriteMaterial = new THREE.SpriteMaterial({
  map: texture, //设置精灵纹理贴图
  transparent:true,//SpriteMaterial默认是true
  
  const geometry = new THREE.BoxGeometry(25, 100, 50);
  geometry.translate(0, 50, 0);
  // mesh顶部中心添加标注,顶部中心坐标是(0,100,0)
  const mesh = new THREE.Mesh(geometry, material);
});

射线拾取模型

1.射线概念

学习Three.js中的射线Ray概念,可以类比数学几何中提到的射线,在三维空间中,一条线把一个点作为起点,然后沿着某个方向无限延伸。

// 创建射线对象Ray
const ray = new THREE.Ray()

// 设置射线起点
ray.origin = new THREE.Vector3(1,0,3);

// 表示射线沿着x轴正方向
ray.direction = new THREE.Vector3(1,0,0);
// 表示射线沿着x轴负方向
ray.direction = new THREE.Vector3(-1,0,0);

2.射线拾取模型

射线投射器Raycaster通过.intersectObjects()方法可以计算出来与自身射线.ray相交的网格模型。

const raycaster = new THREE.Raycaster();
raycaster.ray.origin = new THREE.Vector3(-100, 0, 0);
raycaster.ray.direction = new THREE.Vector3(1, 0, 0);
// 射线发射拾取模型对象
const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);
console.log("射线器返回的对象", intersects);

// intersects.length大于0说明,说明选中了模型
if (intersects.length > 0) {
    console.log("交叉点坐标", intersects[0].point);
    console.log("交叉对象",intersects[0].object);
    console.log("射线原点和交叉点距离",intersects[0].distance);
}

3. 屏幕坐标转标准设备坐标

image.png

image.png

addEventListener('click',function(event){
    const px = event.clientX;
    const py = event.clientY;
    const x = (px / width) * 2 - 1;
    const y = -(py / height) * 2 + 1;
})

4. Raycaster(鼠标点击选中模型)

  • 坐标转化(屏幕坐标转标准设备坐标)
  • 计算射线(.setFromCamera()方法)
  • 射线交叉计算(.intersectObjects()方法)

下面代码的功能是鼠标单击threejs的canvas画布,通过射线投射器Raycaster射线拾取网格模型,被选中拾取到的网格模型改变颜色。

renderer.domElement.addEventListener('click', function (event) {
    // .offsetY、.offsetX以canvas画布左上角为坐标原点,单位px
    const px = event.offsetX;
    const py = event.offsetY;
    //屏幕坐标px、py转WebGL标准设备坐标x、y
    //width、height表示canvas画布宽高度
    const x = (px / width) * 2 - 1;
    const y = -(py / height) * 2 + 1;
    //创建一个射线投射器`Raycaster`
    const raycaster = new THREE.Raycaster();
    //.setFromCamera()计算射线投射器`Raycaster`的射线属性.ray
    // 形象点说就是在点击位置创建一条射线,射线穿过的模型代表选中
    raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
    //.intersectObjects([mesh1, mesh2, mesh3])对参数中的网格模型对象进行射线交叉计算
    // 未选中对象返回空数组[],选中一个对象,数组1个元素,选中两个对象,数组两个元素
    const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);
    console.log("射线器返回的对象", intersects);
    // intersects.length大于0说明,说明选中了模型
    if (intersects.length > 0) {
        // 选中模型的第一个模型,设置为红色
        intersects[0].object.material.color.set(0xff0000);
    }
})

动画库tween.js(*)

TweenJS是一个由JavaScript语言编写的补间动画库,如果需要tweenjs辅助你生成动画,对于任何前端web项目,你都可以选择tweenjs库。

npm安装

npm i @tweenjs/tween.js@^18
import TWEEN from '@tweenjs/tween.js';

tweenjs基本语法

tweenjs功能从语法的角度讲,就是改变自己的参数对象。

const pos = {x: 0,y: 0};
const tween = new TWEEN.Tween(pos);//创建一段tween动画
//经过2000毫秒,pos对象的x和y属性分别从零变化为100、50
tween.to({x: 100,y: 50}, 2000);
//tween动画开始执行
tween.start();

在requestAnimationFrame动画中,tween更新.update(),tween才能正常执行

function loop() {
    TWEEN.update();//tween更新
    requestAnimationFrame(loop);
}

浏览器控制台测试查看tweenjs是否逐渐改变pos对象的x和y属性

function loop() {
    TWEEN.update();
    // 测试tweenjs是否逐渐改变pos对象的x和y属性
    console.log(pos.x,pos.y);
    requestAnimationFrame(loop);
}

tweenjs改变threejs模型对象位置

three.js模型的位置mesh.position属性是一个具有.x.y.z属性的对象,可以直接使用tweenjs直接改变。

//创建一段mesh平移的动画
const tween = new TWEEN.Tween(mesh.position);
//经过2000毫秒,pos对象的x和y属性分别从零变化为100、50
tween.to({x: 100,y: 50}, 2000);
//tween动画开始执行
tween.start(); 

最后不要忘记在渲染循环中更新TWEEN.update();即可。

// 渲染循环
function render() {
    TWEEN.update();
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}
render();

测试模型缩放动画

模型的缩放属性mesh.scale.position属性一样是一个具有.x.y.z属性的对象,你也可以直接用tweenjs动画控制。

new TWEEN.Tween(mesh.scale).to({
    x: 100,
    y: 50
}, 2000).start();

案例模块

渐变线

createGradualLine(fromVector3, toVector3) {
      let arr = [
        fromVector3.x,
        fromVector3.y,
        fromVector3.z,
        toVector3.x,
        toVector3.y,
        toVector3.z,
      ];
      var geometry = new THREE.BufferGeometry();
      var positions = new Float32Array(arr);
      geometry.setAttribute(
        "position",
        new THREE.BufferAttribute(positions, 3)
      );

      // 将颜色从 0x53c7cb 转换成 0xffffff
      var colors = new Float32Array([0.329, 0.78, 0.796, 1, 0, 0]);
      var colorAttribute = new THREE.BufferAttribute(colors, 3);
      geometry.setAttribute("color", colorAttribute);

      // 创建THREE.ShaderMaterial
      var material = new THREE.ShaderMaterial({
        vertexShader: `
    attribute vec3 color;
    varying vec3 vColor;
    void main() {
      vColor = color;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    varying vec3 vColor;
    void main() {
      float distance = distance(vColor, vec3(0.329, 0.780, 0.796));
      float t = smoothstep(0.9, 1.0, distance); // 将0.9改为其他数字,调整颜色渐变的距离
      gl_FragColor = vec4(mix(vec3(0.329, 0.780, 0.796), vec3(1.0, 1.0, 1.0), t), 1.0);
    }
  `
      });

      // 创建THREE.Line
      var line = new THREE.Line(geometry, material);
      return line;
    },

字体渲染的三种方案

  • CSS2DRender

这个会出现一个问题就是文字会在其他网格模型之上

function initCityName(cityName, pos) {
  // 实例化css2d的渲染器
  labelRenderer = new CSS2DRenderer();
  labelRenderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(labelRenderer.domElement)
  //设置样式
  labelRenderer.domElement.style.position = 'absolute';
  labelRenderer.domElement.style.top = '0px';
  labelRenderer.domElement.style.left = '0px';
  labelRenderer.domElement.style.color = 'white'
  labelRenderer.domElement.style.fontSize = '12px'
  // labelRenderer.domElement.style.zIndex = '0';//设置层级
  // 解决添加CSS2DLoader以后轨道控制器不能用的问题
  labelRenderer.domElement.style.pointerEvents = 'none';
  const div = document.createElement('div');
  div.innerHTML = cityName
  const labelObj = new CSS2DObject(div);
  labelObj.name = cityName
  labelObj.position.set(1, 1, 1)
  return labelObj
}
复制代码
  • Canvas+Sprite精灵图实现

将getCanvas返回值输送到createSprite中,并将返回值添加到scene中。这种方法可以使得相机无论如何旋转文字始终朝向屏幕。

function getCanvas(text, fontStyle = "Bold 25px Times New Roman") {
  let canvas = document.createElement('canvas');
  let ctx = canvas.getContext('2d');
  ctx.fillStyle = "#ffffff";
  ctx.font = fontStyle;
  ctx.fillText(text, 0, 120);//写字
  return canvas;
}

function createSprite(canvas, pos) {
  let texture = new THREE.Texture(canvas);
  texture.needsUpdate = true;
  let material = new THREE.SpriteMaterial({
    map: texture
  });
  let mesh = new THREE.Sprite(material);
  mesh.position.set( 0.6, 0.1, 0.2);
  return mesh;
}

复制代码
  • FontLoader + TextGeometry实现

首先引入FontLoader,和字体helvetikerRegular(英文)或者YaHeiRegular(中文) 中文Microsoft YaHei_Regular.json百度网盘地址:

链接:pan.baidu.com/s/1Dp3qh0fM…

提取码:zfzn

如果需要其他字体,可以自行创建。地址gero3.github.io/facetype.js…,选择任意一个中文字体的ttf文件,然后点击生成即可。

import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js';
import helvetikerRegular from 'three/examples/fonts/helvetiker_regular.typeface.json';
//import YaHeiRegular from './assets/font/Microsoft YaHei_Regular.json';
复制代码

直接根据官网(threejs.org/docs/index.… 的写法会出现问题,所以我使用了一下方法(亲测可用)

TextGeometry()第一个参数需要是字符串,如果需要使用变量,可以使用toString()方法转换一下

  var loader = new FontLoader();
  var font = loader.parse(helvetikerRegular);
  //var font = loader.parse(YaHeiRegular);
  
  const material = new THREE.MeshBasicMaterial({ color: 0xffffff });
    const textGeometry = new TextGeometry(cityName, {
      font: font,
      size: 0.8,  //字体大小
      height: 0.1  //字体高度
    });
    const textMesh = new THREE.Mesh(textGeometry, material);
    textMesh.position.set(1, 1, 1);
    scene.add(textMesh);

飞线部分

  • 使用着色器实现
 /**
   * createFlyCurve:创造飞线
   * timerFlyCurve:放到render中让飞线动起来
   */
  const uniforms = {
    u_time: { value: 0.0 },
  };
  
  const clock = new THREE.Clock();
  const timerFlyCurve = () => {
    uniforms.u_time.value = clock.getElapsedTime();
  };
  //  clamp()函数用于将一个值限制在一个指定的范围内
  // 着色器可以写在.glsl文件中使用import引用 比如 import fragmentShader from './glsls/fragmentShader2.frag?raw';
  // fract(val) ,意思为1减abs(val)的小数部分
  const vertexShader = ` 
          varying vec2 vUv;
          attribute float percent;
          uniform float u_time;
          uniform float number;
          uniform float speed;
          uniform float length;
          varying float opacity;
          uniform float size;
          void main()
          {
              vUv = uv;
              vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
              float l = clamp(1.0-length,0.0,1.0);
              gl_PointSize = clamp(fract(percent*number + l - u_time*number*speed)-l ,0.0,1.) * size * (1./length);
              opacity = gl_PointSize/size;
              gl_Position = projectionMatrix * mvPosition;
          }
         `;
  const fragmentShader = `
          #ifdef GL_ES
            precision mediump float;
            #endif
            varying float opacity;
            uniform vec3 color;
            void main(){
                gl_FragColor = vec4(color,1.0);
            }
        `;
  function createFlyCurve(points, speed) {
    let curve = new THREE.CatmullRomCurve3(points);
    const colors = new THREE.Color(0x53c7cb);
    let color = new THREE.Vector3(colors.r, colors.g, colors.b);
    let flyLine = initFlyLine(
      curve,
      {
        speed: speed, // 速度
        color: color, // 颜色
        number: 1, //同时跑动的流光数量
        length: 0.4, //流光线条长度
        size: 5.5, //粗细
      },
      150
    );
    return flyLine;
  }
  function initFlyLine(curve, matSetting, pointsNumber) {
    let points = curve.getPoints(pointsNumber);
    let geometry = new THREE.BufferGeometry().setFromPoints(points);
    const length = points.length;
    let percents = new Float32Array(length);
    for (let i = 0; i < points.length; i += 1) {
      percents[i] = i / length;
    }
    geometry.setAttribute('percent', new THREE.BufferAttribute(percents, 1));
    const lineMaterial = initLineMaterial(matSetting);
    let flyLine = new THREE.Points(geometry, lineMaterial);
    return flyLine;
  }
  
  function initLineMaterial(setting) {
    const number = setting ? Number(setting.number) || 1.0 : 1.0;
    const speed = setting ? Number(setting.speed) || 1.0 : 1.0;
    const length = setting ? Number(setting.length) || 0.5 : 0.5;
    const size = setting ? Number(setting.size) || 3.0 : 3.0;
    const color = setting
      ? setting.color || new THREE.Vector3(0, 1, 1)
      : new THREE.Vector3(0, 1, 1);
    const singleUniforms = {
      u_time: uniforms.u_time,
      number: { type: 'f', value: number },
      speed: { type: 'f', value: speed },
      length: { type: 'f', value: length },
      size: { type: 'f', value: size },
      color: { type: 'v3', value: color },
    };
    const lineMaterial = new THREE.ShaderMaterial({
      uniforms: singleUniforms,
      vertexShader: vertexShader,
      fragmentShader: fragmentShader,
      transparent: true,
    });
    return lineMaterial;
  }
  • 通过计算位置实现
 // 调用
 this.createFlyLine([fromVector3, midVector3, toVector3]);
createFlyLine(pointArr) {
      // 创建轨迹线
      let curve = new THREE.CatmullRomCurve3(pointArr);

      let trackMat = new THREE.MeshBasicMaterial({
        color: color,
      });
      let curveSegment = 100
      let points = curve.getPoints(curveSegment);
      let trackGeo = new THREE.BufferGeometry().setFromPoints(points);
      let flyTrack = new THREE.Line(trackGeo, trackMat);
      this.trackGroup.add(flyTrack);

      // 创建轨迹线上的飞线
      let index = 0;
      let num = curveSegment * 0.1;
      let points2 = points.slice(index, index + num);

      let flyCurve = new THREE.CatmullRomCurve3(points2);
      let newPoints2 = flyCurve.getSpacedPoints(10); //获取更多的点数
      let flyGeo = new THREE.BufferGeometry();
      flyGeo.setFromPoints(newPoints2);
      // 每个顶点对应一个百分比数据attributes.percent 用于控制点的渲染大小
      let percentArr = []; //attributes.percent的数据
      for (let i = 0; i < newPoints2.length; i++) {
        percentArr.push(i / newPoints2.length);
      }
      let percentAttribute = new THREE.BufferAttribute(
        new Float32Array(percentArr),
        1
      );
      flyGeo.attributes.percent = percentAttribute;
      // 点模型渲染几何体每个顶点
      let pointMat = new THREE.PointsMaterial({
        color: color,
        size: 0.2, //点大小
      });
      let flyPoints = new THREE.Points(flyGeo, pointMat);
      flyPoints.num = num;
      flyPoints.index = index;
      flyPoints.points = points
      flyPoints.flyGeo = flyGeo
      flyPoints.curveSegment = curveSegment
      this.flyGroup.add(flyPoints);
      // 修改点材质的着色器源码(注意:不同版本细节可能会稍微会有区别,不过整体思路是一样的)
      pointMat.onBeforeCompile = function (shader) {
        // 顶点着色器中声明一个attribute变量:百分比
        shader.vertexShader = shader.vertexShader.replace(
          "void main() {",
          [
            "attribute float percent;", //顶点大小百分比变量,控制点渲染大小
            "void main() {",
          ].join("\n") // .join()把数组元素合成字符串
        );
        // 调整点渲染大小计算方式
        shader.vertexShader = shader.vertexShader.replace(
          "gl_PointSize = size;",
          ["gl_PointSize = percent * size;"].join("\n") // .join()把数组元素合成字符串
        );
      };

      // if (this.flyGroup.children.length) disposeGroup(this.flyGroup);
    },

render() { 
      this.flyGroup.children.forEach((fly)=>{
        let indexMax = fly.curveSegment - fly.num
        console.log(fly.index)
        if (fly.index > indexMax) fly.index = 0;
        fly.index +=1
        let points3 = fly.points.slice(
          fly.index,
          fly.index + fly.num
        ); //从曲线上获取一段
        var curve = new THREE.CatmullRomCurve3(points3);
        var newPoints2 = curve.getSpacedPoints(fly.curveSegment); //获取更多的点数
        fly.flyGeo.setFromPoints(newPoints2);
      })

      this.renderer.render(this.scene, this.camera);
      requestAnimationFrame(this.render);
      this.stats.update();
    },

波动光环

drawWaveMesh(posVec) {
      const texLoader = new THREE.TextureLoader();
      const wave = require("../assets/img/wave1.png");
      var material = new THREE.MeshBasicMaterial({
        map: texLoader.load(wave),
        transparent: true,
      });
      var geometry = new THREE.PlaneBufferGeometry(1, 1); //默认在XOY平面上
      var mesh = new THREE.Mesh(geometry, material);

      mesh.size = 2; //自定义一个属性,表示mesh静态大小
      mesh._s = Math.random() * 1.0 + 1.0; //自定义属性._s表示mesh在原始大小基础上放大倍数  光圈在原来mesh.size基础上1~1.5倍之间变化
      mesh.scale.set(
        mesh.size * mesh._s,
        mesh.size * mesh._s,
        mesh.size * mesh._s
      );

      mesh.position.set(posVec.x, posVec.y, 0.01); //设置mesh位置

      return mesh;
    },

 render() {
      // 所有波动光圈都有自己的透明度和大小状态,一个波动光圈透明度变化过程是:0~1~0反复循环
      if (waveMeshArr.length) {
        waveMeshArr.forEach(function (mesh) {
          mesh._s += 0.02;
          mesh.scale.set(
            mesh.size * mesh._s,
            mesh.size * mesh._s,
            mesh.size * mesh._s
          );

          if (mesh._s <= 1.5) {
            mesh.material.opacity = (mesh._s - 1) * 2;
          } else if (mesh._s > 1.5 && mesh._s <= 2) {
            mesh.material.opacity = 1 - (mesh._s - 1.5) * 2; //1->0
          } else {
            mesh._s = 1;
          }
        });
      }
      .............
    },