# 基础知识
学习官网 (opens new window) 源码地址 (opens new window) 小程序版 (opens new window)
# 什么是WEBGL?
# WebGL (Web图形库) 是一种JavaScript API,用于在浏览器中呈现交互式3D和2D图形,而无需任何插件
# WebGL通过引入与OpenGL紧密相符合的API,可以在HTML5 元素中使用
# WebGL给我们提供了一系列的图形接口,能够让我们通过js去使用GPU来进行浏览器图形渲染的工具
WebGL与OpenGL的对比:
# WebGL是专为网络环境设计的,它跨平台且主要提供英文支持。WebGL程序的控制代码是用JavaScript编写的
# OpenGL是一个更为通用的跨语言和平台的图形渲染API
# 提供了比WebGL更多的功能,如几何着色器、细分着色器和计算着色器等
# 主要用于桌面应用程序开发,并且是用C语言编写的
# 什么是ThreeJS?
Three.js是一款webGL框架,在WebGL的api基础上,又进行的封装,由于其易用性、开源性被广泛应用
# 什么是Cesium?
Cesium是国外一个基于WebGL编写的地图引擎,支持3D,2D,2.5D形式的地图展示,专注于GIS
# 什么是ThingJS?
ThingJS是对Three.js的进一步封装,无需关心渲染、网格、光线等复杂概念,旨在简化3D应用开发,主要针对物联网领域
# Three.js相关的开源库
- Physijs (opens new window):是一款物理引擎,可以协助WebGL或three.js模拟物理现象,比如重力下落、物体碰撞等
- Stats.js (opens new window):JavaScript性能监控器,同样也可以测试webgl的渲染性能
- dat.gui (opens new window):轻量级的图形用户界面库,可以很容易地创建出能够改变代码变量的界面组件
- Tween.js (opens new window):可以快速创建补间动画,可以非常方便的控制机械、游戏角色运动
- ThreeBSP (opens new window):是一个为了方便对Three.js中的模型进行交集、并集、差集的开源库
# 目录结构
build # 构建完成的Three.js,可以直接引入使用,并有压缩版
docs # Three.js 的官方文档
editor # Three.js的一个网页版的模型编辑器
examples # Three.js的官方案例,如果全都学会,必将成为大神
src # 这里面放置的全是编译Three.js的源文件,每一个.js文件对应帮助文档doc中的一个构造函数
files # 资源文件
manual # 使用手册
playground # 包含基于 Three.js 的示例的存储库,用于学习目的
test # 一些官方测试代码,我们一般用不到
utils # 一些相关插件
其他 # 开发环境搭建、开发所需要的文件,如果不对Three.js进行二次开发,用不到
# jsm文件夹
js和jsm文件夹是对映关系,jsm主要用在es6 import中
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"
# 右手坐标系
# 右手背对着屏幕放置,拇指即指向X轴的正方向。伸出食指和中指
# 如右图所示,食指指向Y轴的正方向,中指所指示的方向即是Z轴的正方向
# 角度弧度互转
Math.PI = 3.1415926 = 180度
弧度 = 弧长 ÷ 半径 # 一个圆的弧度 = 圆周长 ÷ 直径 = 2Π
弧度(radian) = 角度 / 180 * Math.PI # 最大是2Π
角度(degree) = 弧度 * 180 / Math.PI
角度转弧度 # THREE.MathUtils.degToRad(deg)
弧度转角度 # THREE.MathUtils.radToDeg (rad)
# 数学几何
# 三维向量
所有三维向量 Vector3类型的属性都有下列方法
const v3 = new THREE.Vector3(0,0,0);
// 修改某个分量的值
v3.set(10,0,0);
v3.x = 100;
// 设置模型在场景Scene中的位置
mesh.position.set(80,2,10);
// 设置模型xyz方向分别缩放0.5,1.5,2倍
mesh.scale.set(0.5, 1.5, 2)
// 平移方法
mesh.translateX(100);// 沿着x轴正方向平移距离100
mesh.translateY(100);// 沿着y轴正方向平移距离100
mesh.translateZ(100);// 沿着z轴正方向平移距离100
// 沿着自定义的方向移动
const axis = new THREE.Vector3(1, 1, 1);// 向量Vector3对象表示方向
axis.normalize(); // 向量归一化,x、y、z三个方向的和是1
// 沿着axis轴表示方向平移100
mesh.translateOnAxis(axis, 100);
# 向量加法运算
const A = new THREE.Vector3(30, 30, 0);
const B = new THREE.Vector3(100, 50, 0);
const C = new THREE.Vector3();
// addVectors的含义就是参数中两个向量xyz三个分量分别相加
C.addVectors(A,B);
// A和B的x、y、z属性分别相加,相加的结果替换A原来的x、y、z
const D = A.add(B);
// 如果不希望A被改变,且创建一个新的对象表示D点坐标,通过克隆方法.clone()
const D = A.clone().add(B);
# 向量减法运算
const A = new THREE.Vector3(30, 30, 0);
const B = new THREE.Vector3(130,80,0);
const AB = new THREE.Vector3();
// 表示B的xyz三个分量,与A的xyz三个分量分别相减,然后赋值给向量AB
AB.subVectors(B,A);
// 表示B的xyz三个属性分别减去A的xyz三个属性,然后结果赋值给B自身的xyz属性
const AB = B.sub(A);
// 如果希望基于A和B两点位置,生成一个A指向B的向量,可以B克隆一个新对象
const AB = B.clone().sub(A);
# 向量长度
向量不仅可以表示两点之间的距离,还可以表示速度、加速度、力等物理量,大小就是length()
const A = new THREE.Vector3(30, 30, 0);
const B = new THREE.Vector3(130,80,0);
// 两点坐标构建一个向量AB
const AB = B.clone().sub(A);
// 向量长度表示AB两点距离
const L = AB.length();
console.log('L',L);
# 单位向量
单位向量是向量长度.length()为1的向量,可以用单位向量表示向量的方向
const v = new THREE.Vector3(1,0,0);
console.log('向量长度',v.length());
向量归一化,就是等比例缩放向量的xyz三个分量,缩放到向量长度.length()为
const dir = new THREE.Vector3(1, 1, 0);
dir.normalize(); //向量归一化
向量方法 multiplyScalar(50) 表示向量x、y、z三个分量和参数分别相乘
const walk = v.clone().multiplyScalar(50);
# 辅助观察箭头 ArrowHelper
// 绘制一个从A指向B的箭头
const AB = B.clone().sub(A);
const L = AB.length();// AB长度
const dir = AB.clone().normalize();// 单位向量表示AB方向
// 生成箭头从A指向B
const arrowHelper = new THREE.ArrowHelper(dir, A, L)
group.add(arrowHelper);
# 匀速动画(向量表示速度)
const v = new THREE.Vector3(10,0,10);// 物体运动速度
const clock = new THREE.Clock();// 时钟对象
// 渲染循环
function render() {
const time = clock.getElapsedTime();// 获取时钟运行的总时长
const spt = clock.getDelta();// 两帧渲染时间间隔(秒)
// 在spt时间内,以速度v运动的位移量
const dis = v.clone().multiplyScalar(spt);
// 网格模型当前的位置加上spt时间段内运动的位移量
mesh.position.add(dis);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
# 向量点乘 dot
a.dot(b) 是向量a在向量b上的投影长度与向量b相乘,或者说向量a长度 * 向量b长度 * cos(ab夹角)
const a = new THREE.Vector3(10, 10, 0);
const b = new THREE.Vector3(20, 0, 0);
// dot几何含义:向量a长度 * 向量b长度 * cos(ab夹角)
const dot = a.dot(b);
console.log('点乘结果',dot);// 判断结果是不是200
单位向量点乘含义:计算向量夹角余弦值
两个向量的夹角是θ,两个向量的单位向量进行点乘.dot(),返回的结果就是夹角θ的余弦值cos(θ)
const a = new THREE.Vector3(10, 10, 0);
const b = new THREE.Vector3(20, 0, 0);
// a、b向量归一化后点乘
const cos = a.normalize().dot(b.normalize());
console.log('向量夹角余弦值',cos);
夹角余弦值转角度值
// 反余弦计算向量夹角弧度(以弧度表示)
const rad = Math.acos(cos);
// 弧度转角度
const angle = THREE.MathUtils.radToDeg(rad);
console.log('向量夹角角度值',angle);
# 向量叉乘 cross
叉乘也有其它称呼,比如向量积、外积、叉积、矢积
const a = new THREE.Vector3(50, 0, 0);
const b = new THREE.Vector3(30, 0, 30);
// 创建一个向量c,用来保存叉乘结果
const c = new THREE.Vector3();
//向量a叉乘b,结果保存在向量c
c.crossVectors(a,b);
// 或者
const c = a.clone().cross(b);
//叉乘结果是一个向量对象Vector3
console.log('叉乘结果',c);
叉乘结果向量c几何含义
- 一方面是向量方向
- 向量c垂直于向量a和b构成的平面,或者说向量c同时垂直于向量a、向量b
- 向量 b 叉乘 向量 a 的结果与向量 a 叉乘 向量 b 叉乘 的结果方向反过来
- 另一方面是向量长度
- 向量 c 的长度:
c.length() = a.length()*b.length()*sin(θ)
- 三角形的两条边构建两个向量进行叉乘,叉乘的结果c的长度就表示三角形面积的2倍
- 向量 c 的长度:
叉乘计算三角形法线
// 已知三角形三个顶点的坐标,计算三角形法线方向
const p1 = new THREE.Vector3(0, 0, 0);
const p2 = new THREE.Vector3(50, 0, 0);
const p3 = new THREE.Vector3(0, 100, 0);
// 三个顶点构建两个向量,按照三角形顶点的顺序,构建1指向2的向量,2指向3的向量
const a = p2.clone().sub(p1);
const b = p3.clone().sub(p2);
const c = a.clone().cross(b);
c.normalize();//向量c归一化表示三角形法线方向
// 可视化向量a和b叉乘结果:向量c
const arrow = new THREE.ArrowHelper(c, p3, 50, 0xff0000);
mesh.add(arrow);
计算有顶点索引物体的表面积
const pos = geometry.attributes.position;
const index = geometry.index;
console.log('geometry',geometry);
let S = 0;//表示物体表面积
for (var i = 0; i < index.count; i += 3) {
// 获取当前三角形对应三个顶点的索引
const i1 = index.getX(i);
const i2 = index.getX(i + 1);
const i3 = index.getX(i + 2);
//获取三个顶点的坐标
const p1 = new THREE.Vector3(pos.getX(i1), pos.getY(i1), pos.getZ(i1));
const p2 = new THREE.Vector3(pos.getX(i2), pos.getY(i2), pos.getZ(i2));
const p3 = new THREE.Vector3(pos.getX(i3), pos.getY(i3), pos.getZ(i3));
S += AreaOfTriangle(p1, p2, p3);
}
console.log('S',S);
//三角形面积计算
function AreaOfTriangle(p1, p2, p3) {
// 三角形两条边构建两个向量
const a = p2.clone().sub(p1);
const b = p3.clone().sub(p1);
// 两个向量叉乘结果c的几何含义:a.length()*b.length()*sin(θ)
const c = a.clone().cross(b);
// 三角形面积计算
const S = 0.5 * c.length();
return S
}
# 欧拉角 Euler
用来表述物体空间姿态角度的一种数学工具
// x:(可选) 用弧度表示绕x轴旋转量。默认值是 0
// y:(可选) 用弧度表示绕y轴旋转量。默认值是 0
// z:(可选) 用弧度表示绕z轴旋转量。默认值是 0
// order:(可选) 表示旋转顺序的字符串,默认为'XYZ'(必须是大写)
Euler( x:Float, y:Float, z:Float, order:String )
// 创建一个欧拉对象,表示绕着xyz轴分别旋转45度,0度,90度
const Euler = new THREE.Euler( Math.PI/4,0, Math.PI/2);
// 或者
const Euler = new THREE.Euler();
Euler.x = Math.PI / 3; // 绕x轴旋转60度
Euler.y = Math.PI / 3; // 绕y轴旋转60度
Euler.z = Math.PI / 3; // 绕z轴旋转60度
// 绕y轴的角度设置为60度
mesh.rotation.y = Math.PI/3;
// 旋转方法
mesh.rotateX(Math.PI/4);// 绕x轴旋转π/4,默认绕几何体中心旋转
mesh.rotateY(Math.PI/4);// 绕y轴旋转π/4,默认绕几何体中心旋转
mesh.rotateZ(Math.PI/4);// 绕z轴旋转π/4,默认绕几何体中心旋转
// 沿着自定义的方向旋转
const axis = new THREE.Vector3(0,1,0);// 向量axis
mesh.rotateOnAxis(axis,Math.PI/8);// 绕axis轴旋转π/8
# 颜色对象 Color
// 创建一个颜色对象
const color = new THREE.Color();// 默认是纯白色0xffffff
const color = new THREE.Color(0x00ff00);
console.log('查看颜色对象结构', color);// 可以查看rgb的值
// 改变颜色的方法
color.r = 0.0;
color.b = 0.0;
color.setRGB(0,1,0);// RGB方式设置颜色
color.setHex(0x00ff00);// 十六进制方式设置颜色
color.setStyle('#00ff00');// 前端CSS颜色值设置颜色
// 都可以作为.set()的参数
color.set('rgb(0,255,0)');
color.set(0x00ff00);
color.set('#00ff00');
颜色渐变插值
// 通过一个百分比参数可以控制Color1和Color2两种颜色混合的百分比
lerpColors(Color1, Color2, percent)
const c1 = new THREE.Color(0xff0000); //红色
const c2 = new THREE.Color(0x0000ff); //蓝色
const c = new THREE.Color();
c.lerpColors(c1,c2, 0.3);
// c1与c2颜色混合,混合后的rgb值,赋值给c1的.r、.g、.b属性
Color1.lerp(Color2, percent)
const c1 = new THREE.Color(0xff0000); //红色
const c2 = new THREE.Color(0x0000ff); //蓝色
c1.lerp(c2, percent);
# 四元数 Quaternion
四元数Quaternion和欧拉角Euler一样,可以用来计算或表示物体在3D空间中的旋转姿态角度
const quaternion = new THREE.Quaternion();
.setFromAxisAngle() 是四元数的一个方法,可以用来辅助生成表示特定旋转的四元数
// 旋转轴new THREE.Vector3(0,0,1)
// 旋转角度Math.PI/2,绕z轴旋转90度
quaternion.setFromAxisAngle(new THREE.Vector3(0,0,1),Math.PI/2);
.applyQuaternion(quaternion) 通过四元数对Vector3进行旋转
const quaternion = new THREE.Quaternion();
// 绕z轴旋转90度
quaternion.setFromAxisAngle(new THREE.Vector3(0,0,1),Math.PI/2);
// 通过四元数旋转A点:把A点绕z轴旋转90度生成一个新的坐标点B
const B = A.clone().applyQuaternion(quaternion);
console.log('B',B);
Three.js模型对象都有一个属性.quaternion,.quaternion的属性值就是四元数对象Quaternion,改变物体的四元数属性.quaternion,也就是改变物体的姿态角度
const quaternion = new THREE.Quaternion();
quaternion.setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI / 2);
// quaternion表示旋转角度复制给物体.quaternion
mesh.quaternion.copy(quaternion);
# 物体角度属性.rotation和四元数属性.quaternion
Three.js模型对象的角度.rotation和四元数.quaternion属性都是用来表示物体姿态角度的,只是表达形式不同而已,.rotation和.quaternion两个属性的值,一个改变,另一个也会同步改变
const quaternion = new THREE.Quaternion();
quaternion.setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2);
mesh.quaternion.copy(quaternion);
// 四元数属性改变后,查看角度属性(欧拉角)变化
// .quaternion改变,.rotation同步改变
console.log('角度属性',mesh.rotation.z);
# 四元数乘法运算 .multiply()
两个四元数相乘,会得到一个新的四元数,新四元数表示两个旋转的组合旋转
const q1 = new THREE.Quaternion();
q1.setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI / 2);
// 在物体原来姿态基础上,进行旋转
mesh.quaternion.multiply(q1);
.setFromUnitVectors(a, b)可以创建一个四元数,表示从单位向量a表示的方向旋转到单位向量b表示的方向
//飞机初始姿态飞行方向a
const a = new THREE.Vector3(0, 0, -1);
// 飞机姿态绕自身坐标原点旋转到b指向的方向
const b = new THREE.Vector3(-1, -1, -1).normalize();
// a旋转到b构成的四元数
const quaternion = new THREE.Quaternion();
//注意两个参数的顺序
quaternion.setFromUnitVectors(a, b);
// quaternion表示的是变化过程,在原来基础上乘以quaternion即可
mesh.quaternion.multiply(quaternion);
# 模型矩阵
矩阵是图形学中一个比较重要的数学工具,m×n矩阵表示m行n列的矩阵
# 矩阵乘法运算规则
# 平移矩阵
一个点的坐标是(x,y,z),假设沿着X、Y、Z轴分别平移Tx、Ty、Tz,平移后的坐标是(x+Tx,y+Ty,z+Tz)
# 坐标是(x,y,z)转化为齐次坐标坐标是(x,y,z,1),可以用4x1矩阵表示,这种特殊形式,也可以称为列向量
# 在webgpu顶点着色器代码中也可以用四维向量vec4表示
# 缩放矩阵
通过缩放矩阵可以对顶点的齐次坐标进行缩放
# 旋转矩阵
假设一个点的坐标是(x,y,z),经过旋转变换后的坐标为(X,Y,Z)
- 绕Z轴旋转γ角度:z的坐标不变,x、y发生变化
X=xcosγ-ysinγ、Y=xsinγ+ycosγ
# 假设旋转前角度A,对应x和y的值
x = R * cos(A)
y = R * sin(A)
# 假设旋转了γ度,对应X和Y的值
X = R * cos(γ+A)
= R * (cos(γ)cos(A)-sin(γ)sin(A))
= R*cos(A)cos(γ) - R*sin(A)sin(γ)
= xcosγ-ysinγ
Y = R * sin(γ+A)
= R * (sin(γ)cos(A)+cos(γ)sin(A))
= R*cos(A)sin(γ) + R*sin(A)cos(γ)
= xsinγ+ycosγ
# 通过推理得知旋转后的坐标:X=xcosγ-ysinγ、Y=xsinγ+ycosγ
- 绕X轴旋转α角度:x的坐标不变,y、z的坐标发生变化
Y=ycosα-zsinα、Z=ysinα+zcosα
- 绕Y轴旋转β角度:y的坐标不变,z、x的坐标发生变化
Z=zsinβ+xcosβ、X=zcosβ-xsinβ
# 矩阵表示(先平移、后缩放)
假设一个顶点原始坐标(2,0,0),先沿着x轴平移2,变为(4,0,0),再x轴方向缩放10倍,最终坐标是(40,0,0)
# 模型矩阵表示(先平移、后缩放)
先计算所有几何变换对应矩阵的乘积,得到一个模型矩阵,再对顶点坐标进行变换
注意
矩阵的乘法运算,不满足交换律,矩阵顺序,不能随意设置,先发生的平移矩阵,放在后面,后发生的缩放矩阵放在前面
# Three.js矩阵 Matrix4
- 创建4x4矩阵Matrix4对象
const mat4 = new THREE.Matrix4()
- 过4x4矩阵Matrix4的属性.elements设置矩阵的值,比如设置一个平移矩阵
// 平移矩阵,沿着x轴平移50
// 1, 0, 0, x,
// 0, 1, 0, y,
// 0, 0, 1, z,
// 0, 0, 0, 1
const mat4 = new THREE.Matrix4()
// .elements属性值是一个数组,数组的元素就是4x4矩阵的16个数字
// 数字在数组中按照矩阵列的顺序,一列一节输入数组中
mat4.elements=[1,0,0,0, 0,1,0,0, 0,0,1,0, 50, 0, 0, 1];
// .elements属性不设置,默认是单位矩阵
const mat4 = new THREE.Matrix4()
// 默认值单位矩阵
// 1, 0, 0, 0,
// 0, 1, 0, 0,
// 0, 0, 1, 0,
// 0, 0, 0, 1
console.log('.elements默认值', mat4.elements);
- 顶点坐标进行矩阵变换Vector3.applyMatrix4()
# .applyMatrix4()是三维向量Vector3的一个方法,如果Vector3表示一个顶点xyz坐标
# Vector3执行.applyMatrix4()方法意味着通过矩阵对顶点坐标进行矩阵变换,比如平移、旋转、缩放
const p = new THREE.Vector3(50,0,0);
// 矩阵对p点坐标进行平移变换
p.applyMatrix4(mat4);
console.log('查看平移后p点坐标',p);
- 快速生成平移、旋转、缩放矩阵
# threejs提供了一些更为简单的方法,辅助创建各种几何变换矩阵
# 平移矩阵:.makeTranslation(Tx,Ty,Tz)
# 缩放矩阵:.makeScale(Sx,Sy,Sz)
# 绕x轴的旋转矩阵:.makeRotationX(angleX)
# 绕y轴的旋转矩阵:.makeRotationY(angleY)
# 绕z轴的旋转矩阵:.makeRotationZ(angleZ)
// 空间中p点坐标
const p = new THREE.Vector3(50,0,0);
const T = new THREE.Matrix4();
// 平移矩阵
T.makeTranslation(50,0,0);
const R = new THREE.Matrix4();
// 旋转矩阵
R.makeRotationZ(Math.PI/2);
// p点矩阵变换
p.applyMatrix4(T);// 先平移
p.applyMatrix4(R);// 后旋转
mesh.position.copy(p);
# 矩阵乘法 multiply()
这三个方法功能相同,只是语法细节有差异而已
c.multiplyMatrices(a,b) # axb,结果保存在c
a.multiply(b) # axb,保存在a
a.premultiply(b) # bxa,保存在a
比如两个矩阵,一个是平移矩阵T、一个是旋转矩阵R,两个矩阵相乘的结果就表示旋转和平移的复合变换
const T = new THREE.Matrix4();
T.makeTranslation(50,0,0);//平移矩阵
const R = new THREE.Matrix4();
R.makeRotationZ(Math.PI/2);//旋转矩阵
// p点矩阵变换
// p.applyMatrix4(T);//先平移
// p.applyMatrix4(R);//后旋转
// 旋转矩阵和平移矩阵相乘得到一个复合模型矩阵
const modelMatrix = R.clone().multiply(T);
p.applyMatrix4(modelMatrix);
注意
- 先平移后旋转,相乘时是旋转乘平移
- 矩阵乘法一般不满足交换律,R.clone().multiply(T)和T.clone().multiply(R)表示的结果不同
# 模型本地矩阵 .matrix
模型本地矩阵属性.matrix的是一个4x4矩阵Matrix4
console.log('mesh.matrix',mesh.matrix);
当没有对模型进行旋转、缩放、平移的时候,模型本地矩阵属性.matrix的默认值是一个单位矩阵
当改变模型位置.position、缩放.scale或角度.rotation/.quaternion的时候,都会影响.matrix的值
mesh.position.set(2,3,4);
mesh.scale.set(6,6,6);
mesh.updateMatrix();
console.log('本地矩阵',mesh.matrix);
注意
.matrix本质上就是旋转矩阵、缩放矩阵、平移矩阵的复合矩阵
# 世界矩阵 .matrixWorld
- 世界坐标.getWorldPosition(),是该对象及其父对象所有.position属性值的累加
const worldPosition = new THREE.Vector3();
mesh.getWorldPosition(worldPosition)
console.log('世界坐标',worldPosition);
- 世界矩阵属性.matrixWorld是自身及其所有父对象本地矩阵属性.matrix的复合矩阵
mesh.position.set(2,3,4);
const group = new THREE.Group();
group.add(mesh);
group.position.set(2,3,4);
// 执行updateMatrixWorld()方法,模型的本地矩阵和世界属性都会更新
mesh.updateMatrixWorld();
console.log('本地矩阵',mesh.matrix);
console.log('世界矩阵',mesh.matrixWorld);
不执行.updateMatrixWorld(),默认情况下,在执行.render()的时候,会自动获取.position、.scale等属性的值,更新模型的本地矩阵、世界矩阵属性
const scene = new THREE.Scene();
mesh.position.set(2,3,4);
const group = new THREE.Group();
group.position.set(2,3,4);
group.add(mesh);
scene.add(group);
// render渲染时候,会获取模型`.position`等属性更新计算模型矩阵值
renderer.render(scene, camera);
console.log('本地矩阵',mesh.matrix);
console.log('世界矩阵',mesh.matrixWorld);
# 如何使用
# Vue中使用ThreeJS
- npm安装特定版本three.js(注意使用哪个版本,查文档就查对应版本)
npm install three@0.148.0 --save
# typescript中使用需要加上下面:
npm install @types/three@0.148.0 --save
- ES6语法引入three.js核心
import * as THREE from 'three'
- 引入three.js其他扩展库
# 在threejs文件包中examples/jsm目录下,你还可以看到各种不同功能的扩展库
# 一般来说,你项目用到那个扩展库,就引入那个,用不到就不需要引入
# 引入扩展库OrbitControls.js
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
# 引入扩展库GLTFLoader.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
# 扩展库引入——旧版本,比如122, 新版本路径addons替换了examples/jsm
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
# HTML中使用ThreeJS
给script标签设置type="module",也可以在.html文件中使用import方式引入three.js
<script type="module">
// 现在浏览器支持ES6语法,自然包括import方式引入js文件
import * as THREE from './build/three.module.js';
</script>
.html文件引入three.js,最好的方式就是通过配置 type="importmap"
。这样方便直接复制源码
// 具体路径配置,你根据自己文件目录设置
<script type="importmap">
{
"imports": {
"three": "../../../three.js/build/three.module.js"
// 引入threejs扩展库
"three/addons/": "../../../three.js/examples/jsm/"
}
}
</script>
// 配置type="importmap",.html文件也能和项目开发环境一样方式引入threejs
<script type="module">
import * as THREE from 'three';
// 扩展库OrbitControls.js
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 扩展库GLTFLoader.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
// 浏览器控制台测试,是否引入成功
console.log(THREE.Scene);
console.log(OrbitControls);
console.log(GLTFLoader);
</script>
# 整体实例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>第一个three.js文件_WebGL三维场景</title>
<style>
body {
margin: 0;
overflow: hidden;
/* 隐藏body窗口区域滚动条 */
}
</style>
<!--引入three.js三维引擎-->
<script src="http://www.yanhuangxueyuan.com/versions/threejsR92/build/three.js"></script>
<!-- <script src="./three.js"></script> -->
<!-- <script src="http://www.yanhuangxueyuan.com/threejs/build/three.js"></script> -->
</head>
<body>
<script>
//创建场景对象Scene
var scene = new THREE.Scene();
//创建网格模型Geometry
var geometry = new THREE.BoxGeometry(100, 100, 100); //创建一个立方体几何对象Geometry
// var geometry = new THREE.SphereGeometry(60, 40, 40); //创建一个球体几何对象
//创建材质Material
//构造函数的参数是一个对象,对象包含了颜色、透明度等属性
var material = new THREE.MeshLambertMaterial({
color: 0x0000ff
});
//网格模型对象Mesh(物体)
//网格模型对象的参数几何体Geometry、材质Material
var mesh = new THREE.Mesh(geometry, material);
//默认在原点位置
mesh.positon.set(100,100,100);
scene.add(mesh); //网格模型添加到场景中
//创建光源Light
var point = new THREE.PointLight(0xffffff);//点光源
point.position.set(400, 200, 300); //点光源位置
scene.add(point); //点光源添加到场景中
var ambient = new THREE.AmbientLight(0x444444);////环境光
scene.add(ambient);
//创建画布Canvas,定义相机渲染输出的画布尺寸
var width = window.innerWidth; //窗口宽度
var height = window.innerHeight; //窗口高度
var k = width / height; //窗口宽高比
var s = 200; //三维场景显示范围控制系数,系数越大,显示的范围越大
//创建相机Camera
// const camera = new THREE.PerspectiveCamera();//透视投影相机
//创建了一个正射投影相机对象,前四个参数定义的是拍照窗口大小
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
camera.position.set(200, 300, 200); //设置相机位置x、y、z
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
//camera.lookAt(0, 0, 0); //指向坐标原点
//camera.lookAt(mesh.position);//指向mesh对应的位置
//创建渲染器Renderer
var renderer = new THREE.WebGLRenderer();
//this.renderer = new THREE.WebGLRenderer({ antialias: true }) // 是否执行抗锯齿
renderer.setSize(width, height);//设置渲染区域尺寸
renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色及透明度
document.body.appendChild(renderer.domElement); //body元素中插入canvas对象
//执行渲染操作 指定场景、相机作为参数
renderer.render(scene, camera);
</script>
</body>
</html>
# 观察工具
# 辅助观察坐标系 AxesHelper
const axesHelper = new THREE.AxesHelper(150);
scene.add(axesHelper);
# 视角指示器辅助观察 ViewHelper
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper';
const viewHelper = new ViewHelper(camera, renderer.domElement)
let viewHelperDiv = document.createElement('div')
viewHelperDiv.className = 'viewhelper'
viewHelperDiv.style.position = 'absolute'
viewHelperDiv.style.width = '128px'
viewHelperDiv.style.height = '128px'
viewHelperDiv.style.right = '0'
viewHelperDiv.style.bottom = '0'
viewHelperDiv.style.zIndex = '1'
dom.appendChild(viewHelperDiv)
viewHelperDiv.addEventListener( 'click', event => {
event.stopPropagation()
viewHelper.handleClick( event )
})
let clock = new THREE.Clock()
const renderFun = () => {
renderer.clear()
renderer.render(this.scene, camera)
viewHelper.render(renderer)
if ( viewHelper.animating === true ) {
viewHelper.update(delta)
}
requestAnimationFrame(renderFun)
}
renderFun()
# 网格地面辅助观察 GridHelper
// size:坐标格尺寸。默认为 10
// divisions:坐标格细分次数。默认为 10
// colorCenterLine:中线颜色。值可以为 Color 类型, 16进制 和 CSS 颜色名。默认为 0x444444
// colorGrid:坐标格网格线颜色。值可以为 Color 类型, 16进制 和 CSS 颜色名。默认为 0x888888
const gridHelper = new THREE.GridHelper(300, 25, 0x004444, 0x888888);
scene.add(gridHelper);
# 相机可视化辅助观察 CameraHelper
const cameraHelper = new THREE.CameraHelper(camera);
scene.add(cameraHelper);
# 变换控制器 TransformControls
mode # 当前的变换模式。可能的值包括"translate"、"rotate" 和 "scale"。默认为translate
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
let transformControls = new TransformControls(camera, renderer.domElement);
transformControls.addEventListener("change", animate);
// 监听拖动事件,当拖动物体时候,禁用轨道控制器
transformControls.addEventListener("dragging-changed", function (event) {
controls.enabled = !event.value;
});
transformControls.detach();// 从控制器中移除当前3D对象
const target = new Object3D();
transformControls.attach(target)
scene.add(transformControls);
如果不想给AxesHelper、GridHelper、CameraHelper、TransformControls添加变换控制器
axesHelper.raycast = () => {}
gridHelper.raycast = () => {}
CameraHelper.raycast = () => {}
transformControls.raycast = () => {} // 还需要排除TransformControlsPlane类型
// 并且光线投射检查与射线相交的物体时第二个参数设置为false
Raycaster.intersectObject(mesh,false)
# 场景介绍
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x666666); // 设置背景颜色
// 设置背景图片,六个方向的:左右上下前后
const cubeTexture = new THREE.CubeTextureLoader().setPath('/')
.load(['01.jpg','01.jpg','01.jpg','01.jpg','01.jpg','01.jpg']);
scene.background = cubeTexture; // 设置背景贴图
// 或者
var myTextureLoader = new THREE.TextureLoader();
myTextureLoader.load('img/img050.jpg', function (myTexture) {
scene.background = new THREE.WebGLCubeRenderTarget(1024)
// 将一张equirectangular格式的全景图转换到cubemap格式
.fromEquirectangularTexture(myRenderer, myTexture).texture;
});
// 添加线性雾,10:开始应用雾的最小距离,15:结束计算、应用雾的最大距离
scene.fog = new THREE.Fog(0xCCCCCC, 10, 15);
// 添加指数雾,它可以在相机附近提供清晰的视野,且距离相机越远雾的浓度越大,0.1:强度
scene.fog = new THREE.FogExp2(0xCCCCCC, 0.1);
# 相机介绍
# 透视投影相机 PerspectiveCamera:四锥体
- 用来模拟人眼所看到的景象,它是3D场景的中使用得最普遍的投影模式,类似于手电光投影到墙面
- 一般用于人在场景用漫游,或者高俯视整个场景,大部分3D项目
// fov:相机视锥体竖直方向视野角度,从视图的底部到顶部,以角度来表示,默认50
// aspect:相机视锥体水平方向和竖直方向长度比,一般设置为Canvas画布宽高比width / height,默认1
// near:相机视锥体近裁截面相对相机距离,默认0.1
// far:相机视锥体远裁截面相对相机距离,far-near构成了视锥体高度方向,默认2000
PerspectiveCamera(fov:Number, aspect:Number, near:Number, far:Number)
// width和height用来设置Three.js输出的Canvas画布尺寸(像素px)
const width = 800; //宽度
const height = 500; //高度
// 30:视场角度, width / height:Canvas画布宽高比, 1:近裁截面, 3000:远裁截面
const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);
# canvas画布宽高度动态变化
window.onresize = function () {
// 重置渲染器输出画布canvas尺寸
renderer.setSize(window.innerWidth, window.innerHeight);
// 重新调整相机视锥体水平方向和竖直方向长度比
camera.aspect = window.innerWidth / window.innerHeight;
// 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
// 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
// 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
camera.updateProjectionMatrix();
};
# 正交相机 OrthographicCamera:长方体
- 在这种投影模式下,无论物体距离相机距离远或者近,在最终渲染的图片中物体的大小都保持不变
- 这对于渲染2D场景、地图或者UI元素是非常有用的,类似于平行光投影到墙面
// left:摄像机长方体左侧面
// right:摄像机长方体右侧面
// top:摄像机长方体上侧面
// bottom:摄像机长方体下侧面
// near:摄像机长方体近端面
// far:摄像机长方体远端面
OrthographicCamera(left:Number,right:Number,top:Number,bottom:Number,near:Number,far:Number)
# canvas画布宽高度动态变化
window.onresize = function () {
const width = window.innerWidth; //canvas画布宽度
const height = window.innerHeight; //canvas画布高度
// 1. WebGL渲染器渲染的Cnavas画布尺寸更新
renderer.setSize(width, height);
// 2.1.更新相机参数
const k = width / height; //canvas画布宽高比
camera.left = -s*k;
camera.right = s*k;
// 2.2.相机的left, right, top, bottom属性变化了,通知threejs系统
camera.updateProjectionMatrix();
};
# 相机沿着视线方向运动
camera.position.set(202, 123, 125);
camera.lookAt(0, 0, 0);
// 相机目标观察点和相机位置相减,获得一个沿着相机视线方向的向量
const dir = new THREE.Vector3(0 - 202,0 - 123,0 - 125);
// 归一化,获取一个表示相机视线方向的单位向量。
dir.normalize();
obj.getWorldDirection()表示的获取obj对象自身z轴正方向在世界坐标空间中的方向
const dir = new THREE.Vector3();
// 获取相机的视线方向
camera.getWorldDirection(dir);
模型没有任何旋转情况,.getWorldDirection()获取的结果(0,0,1)
const mesh = new THREE.Mesh();
const dir = new THREE.Vector3();
mesh.getWorldDirection(dir);
console.log('dir', dir);
# 改变相机上方向 .up
.up属性默认值是new THREE.Vector3(0,1,0),意思是沿着y轴朝上
console.log('.up默认值',camera.up);
可以改变相机的上方向.up属性值
// 渲染效果:y坐标轴朝下
camera.up.set(0,-1,0)
// 渲染效果:红色x轴向上
camera.up.set(1, 0, 0);
// 渲染效果:蓝色z轴向上
camera.up.set(0, 0, 1);
注意.up属性和.position属性一样,如果在.lookAt()执行之后改变,需要重新执行.lookAt()
camera.lookAt(0,0,0);
camera.up.set(0, 0, 1);// 改变up
camera.lookAt(0,0,0);// 执行lookAt重新计算相机姿态
# 相机控件 OrbitControls
展示模型的时候,可以通过相机控件OrbitControls实现旋转缩放的效果,本质上就是改变了相机的位置
const controls = new OrbitControls(camera, renderer.domElement);
// 阻尼设置
controls.enableDamping = true;
controls.dampingFactor = 0.05;// 阻尼惯性,默认0.05
// 自动旋转
controls.autoRotate = true;
controls.autoRotateSpeed = 0.05;// 旋转速度,默认2.0
// 监听鼠标、键盘事件,如果OrbitControls改变了相机参数,重新调用渲染器渲染三维场景
controls.addEventListener('change', function () {
renderer.render(scene, camera);
});
# OrbitControls旋转缩放限制
controls.enablePan = false; // 禁止右键拖拽
controls.enableZoom = false;// 禁止缩放
controls.enableRotate = false; // 禁止旋转
// 透视投影相机缩放范围
controls.minDistance = 200; // 相机位置与观察目标点最小值
controls.maxDistance = 500; // 相机位置与观察目标点最大值
// 正投影相机缩放范围
controls.minZoom = 0.5;
controls.maxZoom = 2;
controls.getDistance(); // 相机位置与目标观察点距离
// 上下旋转范围
controls.minPolarAngle = 0;// 默认值0
controls.maxPolarAngle = Math.PI;// 默认值Math.PI,设置为90度,看不到底部
// 左右旋转范围
controls.minAzimuthAngle = -Math.PI/2;
controls.maxAzimuthAngle = Math.PI/2;
# 改变相机观察目标点
相机控件OrbitControls会影响lookAt设置,注意手动设置OrbitControls的目标参数
// 改变相机观察目标点
camera.lookAt(1000, 0, 1000);
const controls = new OrbitControls(camera, renderer.domElement);
// 相机控件.target属性在OrbitControls.js内部表示相机目标观察点,默认0,0,0
controls.target.set(1000, 0, 1000);
// update()函数内会执行camera.lookAt(controls.targe)
controls.update();
# 辅助设置相机参数
实际开发的时候,可以通过OrbitControls旋转缩放预览3D模型,辅助你选择合适的相机参数
function render() {
requestAnimationFrame(render);
// 浏览器控制台查看相机位置变化
console.log('camera.position',camera.position);
// 浏览器控制台查看controls.target变化,辅助设置lookAt参数
console.log('controls.target',controls.target);
}
render();
# 渲染器介绍
# Canvas画布插入到任意HTML元素中
<div id="webgl" style="margin-top: 200px;margin-left: 100px;"></div>
<script>
document.getElementById('webgl').appendChild(renderer.domElement);
</script>
# 周期性渲染 requestAnimationFrame
默认每秒钟执行60次,每次执行16.7毫秒
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
// renderer.render(scene, camera); //执行渲染操作
document.body.appendChild(renderer.domElement);
// 渲染函数
function render() {
renderer.render(scene, camera); //执行渲染操作
mesh.rotateY(0.01);//每次绕y轴旋转0.01弧度
requestAnimationFrame(render);//请求再次执行渲染函数render,渲染下一帧
}
render();
注意
设置了周期性渲染,OrbitControls就不用再通过change执行renderer.render(scene, camera)了
毕竟渲染循环一直在执行renderer.render(scene, camera);
# 锯齿模糊处理
// 设置渲染器锯齿属性
renderer.antialias = true
// 获取你屏幕对应的设备像素比
const devicePixelRatio = window.devicePixelRatio;
// 告诉threejs,以免渲染模糊问题
renderer.setPixelRatio(devicePixelRatio);
# 渲染器的输出编码
默认为THREE.SRGBColorSpace,需要和纹理对象Texture颜色空间属性一直
// THREE.NoColorSpace = ""
// THREE.SRGBColorSpace = "srgb"
// THREE.LinearSRGBColorSpace = "srgb-linear"
renderer.outputColorSpace = THREE.SRGBColorSpace
# 设置渲染效果
// 即使贴图不是 HDR,也可以塑造更真实的效果
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;// 调整曝光度
# 设置背景透明度
renderer.setClearAlpha(0.8); // 设置透明度
renderer.setClearColor(0xb9d3ff, 0.4); // 设置背景颜色和透明度
# 渲染结果保存为图片
// WebGL渲染器设置
const renderer = new THREE.WebGLRenderer({
//想把canvas画布上内容下载到本地,需要设置为true
preserveDrawingBuffer:true,
});
const link = document.createElement('a');
// 通过超链接herf属性,设置要保存到文件中的数据
const canvas = renderer.domElement; //获取canvas对象
link.href = canvas.toDataURL("image/png");
canvas.toDataURL("image/png");
canvas.toDataURL("image/jpeg");
canvas.toDataURL("image/bmp");
# 查看渲染帧率stats.js
通过stats.js库可以查看three.js当前的渲染性能
//引入性能监视器stats.js
import Stats from 'three/addons/libs/stats.module.js';
//创建stats对象
const stats = new Stats();
//stats.domElement:web页面上输出计算结果,一个div元素,
document.body.appendChild(stats.domElement);
// 渲染函数
function render() {
//requestAnimationFrame循环调用的函数中调用方法update(),来刷新时间
stats.update();
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
}
render();
stats方法setMode(mode)可以更换不同的显示模式
// 显示:渲染帧率 刷新频率,一秒渲染次数
stats.setMode(0);//默认模式
//显示:渲染周期 渲染一帧多长时间(单位:毫秒ms)
stats.setMode(1);
注意
受电脑性能以及需要渲染的物体数量的影响,渲染帧率会自动变化
# 几何体介绍
# 曲线 Curve
threejs提供了很多常用的曲线或直线API,可以直接使用。这些API曲线都有一个共同的父类Curve
# 2D曲线 # 3D曲线
# 直线 LineCurve # 直线 LineCurve3
# 圆弧 ArcCurve
# 椭圆 EllipseCurve
# 二维样条曲线 SplineCurve # 三维样条曲线 CatmullRomCurve3
# 二维贝塞尔曲线 # 三维贝塞尔曲线
# 二次 QuadraticBezierCurve # 二次 QuadraticBezierCurve3
# 三次 CubicBezierCurve # 三次 CubicBezierCurve3
# 直线 LineCurve
2D直线线段LineCurve,参数是表示x、y坐标的二维向量Vector2对象
new THREE.LineCurve(new THREE.Vector2(), new THREE.Vector2());
3D直线线段LineCurve3,参数是表示x、y、z坐标的三维向量Vector3对象
new THREE.LineCurve3(new THREE.Vector3(), new THREE.Vector3());
# 椭圆弧线 EllipseCurve
椭圆曲线x和y方向半径相同,就是一个圆的效果
// aX, aY 椭圆中心坐标
// xRadius 椭圆x轴半径
// yRadius 椭圆y轴半径
// aStartAngle 弧线开始角度,从x轴正半轴开始,默认0,弧度单位
// aEndAngle 弧线结束角度,从x轴正半轴算起,默认2 x Math.PI,弧度单位
// aClockwise 是否顺时针绘制,默认值为false
EllipseCurve( aX, aY, xRadius,yRadius, aStartAngle, aEndAngle, aClockwise )
绘制一个椭圆曲线的流程
// 1、定义椭圆
const ellipseCurve = new THREE.EllipseCurve(0, 0, 100, 50);
// 2、获取顶点数据 getPoints
const points = ellipseCurve.getPoints(50);
// 3、提取曲线坐标数据
const geometry1 = new THREE.BufferGeometry().setFromPoints( points );
// 4、线模型调用线材质绘制曲线
const material = new THREE.LineBasicMaterial({ color: 0x00fffff });
// 5、线模型
const line = new THREE.Line(geometry1, material);
scene.add(line)
通过.getSpacedPoints()和.getPoints()一样也可以从曲线Curve上返回一系列曲线上的顶点坐标
getSpacedPoints() # 是按照曲线长度等间距返回顶点数据
getPoints() # 会考虑曲线斜率变化,斜率变化快的位置返回的顶点更密集
getPoint(1/100) # 获取100个顶点中的第一个
# 圆弧线 ArcCurve
圆弧线 ArcCurve 的父类是椭圆弧线 EllipseCurve ,语法和椭圆弧线 EllipseCurve 相似
// aX, aY 圆心坐标
// aRadius 圆弧半径
// aStartAngle 弧线开始角度,从x轴正半轴开始,默认0,弧度单位
// aEndAngle 弧线结束角度,从x轴正半轴算起,默认2 x Math.PI,弧度单位
// aClockwise 是否顺时针绘制,默认值为false
ArcCurve( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise )
# 三维样条曲线 CatmullRomCurve3
在三维空间中随意设置几个顶点坐标,就可以生成一条穿过这几个点的光滑曲线
// points:Vector3点数组
// closed:该曲线是否闭合,默认值为false
// curveType:曲线的类型,默认值为centripetal,可能的值为centripetal、chordal和catmullrom
// tension:曲线的张力,默认为0.5
CatmullRomCurve3( points:Array, closed:Boolean, curveType:String, tension:Float )
// 三维向量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);
# 二维样条曲线 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);
# 二维贝塞尔曲线
- 创建一条平滑的二维二次贝塞尔曲线, 由起点、终点和一个控制点所定义
// v0:起点
// v1:中间的控制点
// v2:终点
QuadraticBezierCurve( v0:Vector2, v1:Vector2, v2:Vector2 )
const curve = new THREE.QuadraticBezierCurve(
new THREE.Vector2( -10, 0 ),
new THREE.Vector2( 20, 15 ),
new THREE.Vector2( 10, 0 )
);
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
const curveObject = new THREE.Line( geometry, material );
可以发现贝塞尔曲线经过p0、p2两个点,但是不经过p1点,贝塞尔曲线与直线p01和p12相切
- 二维三次贝塞尔曲线与二维二次贝赛尔曲线区别就是多了一个控制点
// v0:起点
// v1:第一个控制点
// v2:第二个控制点
// v3:终点
CubicBezierCurve ( v0:Vector2, v1:Vector2, v2:Vector2, v3:Vector2 )
const curve = new THREE.CubicBezierCurve(
new THREE.Vector2( -10, 0 ),
new THREE.Vector2( -5, 15 ),
new THREE.Vector2( 20, 15 ),
new THREE.Vector2( 10, 0 )
);
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
const curveObject = new THREE.Line( geometry, material );
# 三维贝塞尔曲线
- 三维二次贝赛尔曲线与二维二次贝赛尔曲线区别就是多了一个维度,参数是三维向量对象Vector3
// v0:起点
// v1:中间的控制点
// v2:终点
QuadraticBezierCurve3( v0:Vector3, v1:Vector3, v2:Vector3 )
const curve = new THREE.QuadraticBezierCurve3(
new THREE.Vector3( -10, 0, 0 ),
new THREE.Vector3( 20, 15, 0 ),
new THREE.Vector3( 10, 0, 0 )
);
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
const curveObject = new THREE.Line( geometry, material );
- 三维三次贝赛尔曲线与二维三次贝塞尔曲线区别就是多了一个维度,参数是三维向量对象Vector3
// v0:起点
// v1:第一个控制点
// v2:第二个控制点
// v3:终点
CubicBezierCurve3( v0:Vector3, v1:Vector3, v2:Vector3, v3:Vector3 )
const curve = new THREE.CubicBezierCurve3(
new THREE.Vector3( -10, 0, 0 ),
new THREE.Vector3( -5, 15, 0 ),
new THREE.Vector3( 20, 15, 0 ),
new THREE.Vector3( 10, 0, 0 )
);
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
const curveObject = new THREE.Line( geometry, material );
# 组合曲线 CurvePath
通过threejs组合曲线CurvePath对象,你可以把直线、圆弧、贝塞尔等线条拼接为一条曲线
// 一个圆弧和直线组合拼接的的U形效果
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);
//组合曲线上获取点
const pointsArr = CurvePath.getPoints(16);
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(pointsArr); //读取坐标数据赋值给几何体顶点
# 多边形轮廓 Shape
是一组二维向量Vector2创建的,父类是Path,Path的父类是CurvePath
Path提供了直线、圆弧、贝塞尔、样条等绘制方法
# 属性
currentPoint # 当前点,默认值Vector2(0,0)
holes: Array # 孔洞
# 方法
moveTo(x, y) # 将当前点移动到x, y
lineTo(x, y) # 将当前点连接一条直线到x,y
arc(x, y, radius, startAngle, endAngle, clockwise) # 相对圆弧,圆心是相对当前点而言
absarc(x, y, radius, startAngle, endAngle, clockwise) # 绝对圆弧,圆心是相对原点而言
ellipse(x, y, xRadius, yRadius, startAngle, endAngle, clockwise, rotation) # 相对圆
absellipse(x, y, xRadius, yRadius, startAngle, endAngle, clockwise, rotation) # 绝对圆
splineThru (points:Array) # 添加二维样条曲线
quadraticCurveTo(cpX:Float, cpY:Float, x:Float, y:Float) # 从当前点创建一条二维二次曲线
bezierCurveTo(cp1X:Float, cp1Y:Float, cp2X:Float, cp2Y:Float, x:Float, y:Float) #二维三次曲线
注意
只有二维的 ShapeGeometry(形状)和三维的 ExtrudeGeometry(拉伸体)可以使用 Shape 类型
// 绘制一个矩形轮廓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)
// ShapeGeometry填充Shape获得一个平面几何体
const geometry = new THREE.ShapeGeometry(shape);
// ExtrudeGeometry拉伸Shape获得一个长方体几何体
const geometry = new THREE.ExtrudeGeometry(shape, {
depth:20,//拉伸长度
bevelEnabled:false,//禁止倒角
});
设置内孔的轮廓,形状上的孔洞
const path1 = new THREE.Path();// 圆孔1
path1.absarc(20, 20, 10);
const path2 = new THREE.Path();// 圆孔2
path2.absarc(80, 20, 10);
const path3 = new THREE.Path();// 方形孔
path3.moveTo(50, 50);
path3.lineTo(80, 50);
path3.lineTo(80, 80);
path3.lineTo(50, 80);
//三个内孔轮廓分别插入到holes属性中
shape.holes.push(path1, path2,path3);
# 平面几何
# 圆形 CircleGeometry
// radius:圆形的半径,默认值为1
// segments:分段(三角面)的数量,最小值为3,默认值为32
// thetaStart:第一个分段的起始角度,默认为0
// thetaLength:圆形扇区的中心角,通常被称为“θ”(西塔)。默认值是2*Pi
CircleGeometry(radius:Float, segments:Integer, thetaStart:Float, thetaLength:Float)
# 平面 PlaneGeometry
// width:平面沿着X轴的宽度。默认值是1
// height:平面沿着Y轴的高度。默认值是1
// widthSegments:(可选)平面的宽度分段数,默认值是1
// heightSegments:(可选)平面的高度分段数,默认值是1
PlaneGeometry(width:Float, height:Float, widthSegments:Integer, heightSegments:Integer)
# 圆环 RingGeometry
// innerRadius:内部半径,默认值为0.5
// outerRadius:外部半径,默认值为1
// thetaSegments:圆环的分段数。这个值越大,圆环就越圆。最小值为3,默认值为32
// phiSegments:最小值为1,默认值为8
// thetaStart:起始角度,默认值为0
// thetaLength:圆心角,默认值为Math.PI * 2
RingGeometry(innerRadius:Float, outerRadius:Float, thetaSegments:Integer,
phiSegments:Integer, thetaStart:Float, thetaLength:Float)
# 形状 ShapeGeometry
已知一个多边形的外轮廓坐标,想通过这些外轮廓坐标生成一个多边形几何体平面
// shapes:一个单独的shape,或者一个包含形状的Array
// curveSegments - Integer - 每一个形状的分段数,默认值为12
ShapeGeometry(shapes:Array, curveSegments:Integer)
// 一组二维向量表示一个多边形轮廓坐标
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);
// 把Shape作为ShapeGeometry的参数,形成一个多边形平面几何体
const geometry = new THREE.ShapeGeometry(shape);
const material = new THREE.MeshLambertMaterial({
wireframe:true,
});
# 立体几何
# 立方体 BoxGeometry
// width:X轴上面的宽度,默认值为1
// height:Y轴上面的高度,默认值为1
// depth:Z轴上面的深度,默认值为1
// widthSegments:(可选)宽度的分段数,默认值是1
// heightSegments:(可选)高度的分段数,默认值是1
// depthSegments:(可选)深度的分段数,默认值是1
BoxGeometry(width:Float, height:Float, depth:Float,
widthSegments:Integer, heightSegments:Integer, depthSegments:Integer)
# 球体 SphereGeometry
// radius:球体半径,默认为1
// widthSegments:水平分段数(沿着经线分段),最小值为3,默认值为32
// heightSegments:垂直分段数(沿着纬线分段),最小值为2,默认值为16
// phiStart:指定水平(经线)起始角度,默认值为0
// phiLength:指定水平(经线)扫描角度的大小,默认值为 Math.PI * 2
// thetaStart:指定垂直(纬线)起始角度,默认值为0
// thetaLength:指定垂直(纬线)扫描角度大小,默认值为 Math.PI
SphereGeometry(radius:Float, widthSegments:Integer, heightSegments:Integer,
phiStart:Float, phiLength:Float, thetaStart:Float, thetaLength:Float)
# 圆柱体 CylinderGeometry
// radiusTop:圆柱的顶部半径,默认值是1
// radiusBottom:圆柱的底部半径,默认值是1
// height:圆柱的高度,默认值是1
// radialSegments:圆柱侧面周围的分段数,默认为32
// heightSegments:圆柱侧面沿着其高度的分段数,默认值为1
// openEnded:指明该圆柱的底面是开放的还是封顶的。默认值为false,即其底面默认是封顶的
// thetaStart:第一个分段的起始角度,默认为0
// thetaLength:圆柱底面圆扇区的中心角,通常被称为θ(西塔)。默认值是2*Pi
CylinderGeometry(radiusTop:Float, radiusBottom:Float, height:Float,
radialSegments:Integer, heightSegments:Integer, openEnded:Boolean,
thetaStart:Float, thetaLength:Float)
圆锥 ConeGeometry
如果 radiusTop 或者 radiusBottom 设置成0则成为圆锥
# 胶囊体 CapsuleGeometry
// radius:胶囊半径。可选的; 默认值为1
// length:中间区域的长度。可选的; 默认值为1
// capSegments:构造盖子的曲线部分的个数。可选的; 默认值为4
// radialSegments:覆盖胶囊圆周的分离的面的个数。可选的; 默认值为8
CapsuleGeometry(radius:Float, length:Float, capSubdivisions:Integer, radialSegments:Integer)
# 管道体 TubeGeometry
// path:Curve - 一个由基类Curve继承而来的3D路径
// tubularSegments:Integer - 组成这一管道的分段数,默认值为64
// radius:Float - 管道的半径,默认值为1
// radialSegments:Integer - 管道横截面的分段数目,默认值为8
// closed:Boolean 管道的两端是否闭合,默认值为false
TubeGeometry(path:Curve, tubularSegments:Integer, radius:Float, radialSegments:Integer,
closed:Boolean)
// 三维样条曲线
const path = 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)
]);
// path:路径 40:沿着轨迹细分数 2:管道半径 25:管道截面圆细分数
const geometry = new THREE.TubeGeometry(path, 40, 2, 25);
# 圆环体 TorusGeometry
// radius - 环面的半径,从环面的中心到管道横截面的中心。默认值是1
// tube:管道的半径,默认值为0.4
// radialSegments:管道横截面的分段数,默认值为12
// tubularSegments:管道的分段数,默认值为48
// arc:圆环的圆心角(单位是弧度),默认值为Math.PI * 2
TorusGeometry(radius:Float, tube:Float, radialSegments:Integer,
tubularSegments:Integer, arc:Float)
# 圆环扭结体 TorusKnotGeometry
// radius - 圆环的半径,默认值为1
// tube:管道的半径,默认值为0.4
// tubularSegments:管道的分段数量,默认值为64
// radialSegments:横截面分段数量,默认值为8
// p:这个值决定了几何体将绕着其旋转对称轴旋转多少次,默认值是2
// q:这个值决定了几何体将绕着其内部圆环旋转多少次,默认值是3
TorusKnotGeometry(radius:Float, tube:Float, tubularSegments:Integer,
radialSegments:Integer, p:Integer, q:Integer)
# 多面体 PolyhedronGeometry
这个类将一个顶点数组投射到一个球面上,之后将它们细分为所需的细节级别
// vertices:一个顶点Array(数组):[1,1,1, -1,-1,-1, ... ]
// indices:一个构成面的索引Array(数组), [0,1,2, 2,3,0, ... ]
// radius:Float - 最终形状的半径
// detail:Integer - 将对这个几何体细分多少个级别。细节越多,形状就越平滑
PolyhedronGeometry(vertices:Array, indices:Array, radius:Float, detail:Integer)
这个类由DodecahedronGeometry、IcosahedronGeometry、OctahedronGeometry和TetrahedronGeometry 所使用,以生成它们各自的几何结构
// 四面缓冲几何体 TetrahedronGeometry
TetrahedronGeometry(radius:Float, detail:Integer)
// 八面体 OctahedronGeometry
OctahedronGeometry(radius:Float, detail:Integer)
// 十二面体 DodecahedronGeometry
DodecahedronGeometry(radius:Float, detail:Integer)
// 二十面体 IcosahedronGeometry
IcosahedronGeometry(radius:Float, detail:Integer)
# 旋转成型 LatheGeometry
利用一个2D轮廓,绕着Y轴进行旋转变换生成一个3D的几何体曲面,创建具有轴对称的网格,比如花瓶
// points:一个Vector2对象数组。每个点的X坐标必须大于0
// segments:要生成的车削几何体圆周分段的数量,默认值是12
// phiStart:以弧度表示的起始角度,默认值为0。
// phiLength:车削部分的弧度范围,2PI将是一个完全闭合的车削几何体,小于2PI是部分的。默认值是2PI
LatheGeometry(points:Array, segments:Integer, phiStart:Float, phiLength:Float)
// 通过二维样条曲线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);
# 拉伸体 ExtrudeGeometry
是基于一个基础的平面轮廓Shape进行变换,生成一个几何体
// shapes:形状或者一个包含形状的数组
// options:一个包含多个参数的对象
ExtrudeGeometry(shapes:Array, options:Object)
// 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, //拉伸长度
// 圆角配置
bevelThickness: 5, // 角尺寸:拉伸方向
bevelSize: 5, // 角尺寸:垂直拉伸方向
bevelSegments: 20, // 圆角:倒角细分精度,默认3
// 直角配置
bevelSegments: 1, // 无倒角
}
);
也可以让一个平面轮廓Shape沿着曲线扫描成型
// 扫描轮廓: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// 沿着路径细分精度,越大越光滑
}
);
# 文本缓冲几何体 TextGeometry
父类是ExtrudeGeometry
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js';
const loader = new FontLoader();
loader.load('fonts/helvetiker_regular.typeface.json', function (font) {
const geometry = new TextGeometry('Hello three.js!', {
font: font, // 字体
size: 80, // 大小
depth: 5, // 高度
curveSegments: 12, // 曲线上点的数量。默认值为12
bevelEnabled: true, // 是否开启斜角,默认为false
bevelThickness: 10, // 文本上斜角的深度,默认值为20
bevelSize: 8, // 斜角与原始文本轮廓之间的延伸距离。默认值为8
bevelSegments: 5 // 斜角的分段数。默认值为3
});
});
# 边缘几何体 EdgesGeometry
作为一个辅助对象来查看geometry的边界线,参数是 geometry
// 外部gltf模型设置材质和边线
loader.load("../建筑模型.gltf", function (gltf) {
// 递归遍历设置每个模型的材质,同时设置每个模型的边线
gltf.scene.traverse(function (obj) {
if (obj.isMesh) {
// 模型材质重新设置
obj.material = new THREE.MeshLambertMaterial({
color: 0x004444,
transparent: true,
opacity: 0.5,
});
// 模型边线设置
const edges = new THREE.EdgesGeometry(obj.geometry);
const edgesMaterial = new THREE.LineBasicMaterial({
color: 0x00ffff,
})
const line = new THREE.LineSegments(edges, edgesMaterial);
obj.add(line);
}
});
model.add(gltf.scene);
})
# 网格几何体 WireframeGeometry
作为一个辅助对象来对一个geometry以三角形线框的形式进行查看
const geometry = new THREE.SphereGeometry( 100, 100, 100 );
const wireframe = new THREE.WireframeGeometry( geometry );
const line = new THREE.LineSegments( wireframe );
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;
scene.add( line );
# 缓冲几何体
BufferGeometry是没有任何形状的空几何体,可通过顶点数据自定义任何几何形状,是所有几何体的父类
const geometry = new THREE.BufferGeometry();
# 属性缓冲区对象 BufferAttribute
这个类用于存储与缓冲几何体相关联的属性
例如:顶点位置向量、面片索引、法向量、颜色值、UV坐标以及任何自定义 attribute
顶点位置数据 # geometry.attributes.position
顶点法向量数据 # geometry.attributes.normal
顶点UV数据 # geometry.attributes.uv
顶点颜色数据 # geometry.attributes.color
# 顶点位置 geometry.attributes.position
//通过javascript类型化数组Float32Array创建一组xyz坐标数据用来表示几何体的顶点坐标
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坐标
]);
// 创建缓冲区对象
// 三个为一组,表示一个顶点的xyz坐标
const attribute = new THREE.BufferAttribute(vertices,3);
// 设置几何体attributes属性的位置属性
geometry.attributes.position = attribue;
// 也可以使用如下方法
// 把数组vertices里面的坐标数据提取出来,赋值给 geometry.attributes.position 属性
geometry.setFromPoints(vertices);
// 点渲染模式
const material = new THREE.PointsMaterial({
color: 0xffff00,
size: 10.0 //点对象像素尺寸
});
//把几何体渲染为点
const points = new THREE.Points(geometry, material); //点模型对象
# 顶点索引 geometry.index
// 原始顶点位置坐标数据
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坐标
]);
// 把三角形重复的顶点位置坐标删除
const vertices = new Float32Array([
0, 0, 0, //顶点1坐标
80, 0, 0, //顶点2坐标
80, 80, 0, //顶点3坐标
0, 80, 0, //顶点4坐标
]);
// 通过javascript类型化数组Uint16Array创建顶点索引数据
const indexes = new Uint16Array([
// 下面索引值对应顶点位置数据中的顶点坐标
0, 1, 2, 0, 2, 3,
])
// 索引数据赋值给几何体的index属性
geometry.index = new THREE.BufferAttribute(indexes, 1); //1个为一组
# 顶点法线 geometry.attributes.normal
使用受光照影响的材质,几何体BufferGeometry需要定义顶点法线数据
// MeshBasicMaterial不受光照影响
// 使用受光照影响的材质,几何体Geometry需要定义顶点法线数据
const material = new THREE.MeshLambertMaterial({
color: 0x0000ff,
side: THREE.DoubleSide, //两面可见
});
Three.js中法线是通过顶点定义,默认情况下,每个顶点都有一个法线数据
// 无顶点索引,矩形平面,无索引,两个三角形,6个顶点
const normals = new Float32Array([
0, 0, 1, //顶点1法线( 法向量 )
0, 0, 1, //顶点2法线
0, 0, 1, //顶点3法线
0, 0, 1, //顶点4法线
0, 0, 1, //顶点5法线
0, 0, 1, //顶点6法线
]);
// 设置几何体的顶点法线属性.attributes.normal
geometry.attributes.normal = new THREE.BufferAttribute(normals, 3);
// 有顶点索引,矩形平面,有索引,两个三角形,有2个顶点重合,有4个顶点
const normals = new Float32Array([
0, 0, 1, //顶点1法线( 法向量 )
0, 0, 1, //顶点2法线
0, 0, 1, //顶点3法线
0, 0, 1, //顶点4法线
]);
// 设置几何体的顶点法线属性.attributes.normal
geometry.attributes.normal = new THREE.BufferAttribute(normals, 3);
自动计算法向量
geometry.computeVertexNormals();
顶点法线辅助器 VertexNormalsHelper
import { VertexNormalsHelper } from 'three/addons/helpers/VertexNormalsHelper.js';
// 这里的参数是BufferGeometry,不是几何体
const vertexNormalsHelper= new VertexNormalsHelper(bufferPlane, 8.2, 0xff0000);
scene.add(vertexNormalsHelper);
# 顶点UV geometry.attributes.uv
顶点UV坐标的作用是从纹理贴图上提取像素映射到网格模型Mesh的几何体表面上
// 浏览器控制台查看常用几何体默认的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坐标 geometry.attributes.uv 和顶点位置坐标 geometry.attributes.position 是一一对应的
顶点UV坐标可以在0~1.0之间任意取值,纹理贴图左下角对应的UV坐标是(0,0),右上角对应的坐标(1,1)
const uvs = new Float32Array([
0, 0, //图片左下角
1, 0, //图片右下角
1, 1, //图片右上角
0, 1, //图片左上角
]);
// 设置几何体attributes属性的位置uv属性
geometry.attributes.uv = new THREE.BufferAttribute(uvs, 2); //2个为一组,表示一个顶点的纹理坐标
UV顶点坐标可以根据需要在0~1之间任意设置,主要看你想把图片的哪部分映射到几何体表面上
// 获取纹理贴图四分之一
const uvs = new Float32Array([
0, 0,
0.5, 0,
0.5, 0.5,
0, 0.5,
]);
划分顶点组设置不同材质
// 投置2个顶点组,形成2个材质
geometry.addGroup(0,3,0);
geometry.addGroup(3,3,1);
const material1 = new THREE.MeshBasicMaterial({})
const material2 = new THREE.MeshBasicMaterial({})
const plane = new THREE.Mesh(geometry,[material1,material2])
通过纹理对象的偏移属性.offset可以实现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();
# 顶点颜色 geometry.attributes.color
与几何体BufferGeometry顶点位置数据一一对应,每个点对应一个位置数据,同时对应一个颜色数据
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);
// 设置几何体attributes属性的颜色color属性
const colors = new Float32Array([
1, 0, 0, //顶点1颜色
0, 0, 1, //顶点2颜色
0, 1, 0, //顶点3颜色
]);
//3个为一组,表示一个顶点的颜色数据RGB
geometry.attributes.color = new THREE.BufferAttribute(colors, 3);
如果希望顶点颜色geometry.attributes.color起作用,需要设置材质属性vertexColors:true
const material = new THREE.MeshBasicMaterial({
//使用顶点颜色数据,color属性可以不用设置
vertexColors:true,//默认false,设置为true表示使用顶点颜色渲染
size: 20.0, //点对象像素尺寸
});
const points = new THREE.Mesh(geometry, material); //点模型对象
# 旋转、缩放、平移几何体
这些方法本质上都是改变几何体的顶点数据
// 几何体xyz三个方向都放大2倍
geometry.scale(2, 2, 2);
// 几何体沿着x轴平移50,没有translateX,那是模型的方法
geometry.translate(50, 0, 0);
// 几何体绕着x轴旋转45度,默认绕几何体中心旋转
geometry.rotateX(Math.PI / 4);
//居中:已经偏移的几何体居中,执行.center(),可以看到几何体重新与坐标原点重合
geometry.center();
# 模型介绍
模型即物体,父类为Object3D
# 网格模型 Mesh
表示基于以三角形为多边形网格的物体的类。同时也作为其他类的基类,例如SkinnedMesh
// geometry(可选):BufferGeometry的实例
// material(可选):一个Material,或是一个包含有Material的数组,默认值是一个MeshBasicMaterial
Mesh( geometry:BufferGeometry, material:Material )
网格模型三角形:正反面,默认反面看不见
# 眼睛(相机)对着三角形的一个面
# 如果三个顶点的顺序是逆时针方向,该面视为正面
# 如果三个顶点的顺序是顺时针方向,该面视为反面
// 一个矩形平面,通过两个三角形拼接而成,并且保证矩形平面两个三角形的正面是一样的
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坐标
]);
网格模型:双面可见
new THREE.MeshBasicMaterial({
side: THREE.FrontSide, //默认只有正面可见
side: THREE.BackSide, //背面可见
side: THREE.DoubleSide, //两面可见
});
# 点模型 Points
一个用于显示点的类
// geometry(可选):BufferGeometry的实例
// material(可选):一个Material,默认值是一个PointsMaterial
Points( geometry:BufferGeometry, material:Material )
# 线模型 Line
- 环线(LineLoop):闭合线条
- 线段(LineSegments):非连续的线条
// geometry(可选):BufferGeometry的实例
// material(可选):一个Material,默认值是一个具有随机颜色的LineBasicMaterial
Line( geometry:BufferGeometry, material:Material )
# 精灵模型 Sprite
- 精灵是一个总是面朝着摄像机的平面,通常含有使用一个半透明的纹理,不需要创建几何体Geometry
- 精灵模型Sprite默认是一个矩形形状,默认长宽都是1,默认在坐标原点位置
// material(可选):是SpriteMaterial的一个实例
Sprite( material:Material )
精灵模型Sprite和Mesh一样具有位置.position和缩放.scale属性
sprite.position.set(0,50,0);
// 缩放只需要设置x、y两个分量就可以,z方向默认值就行
sprite.scale.set(50, 25, 1);
# 实例化网格 InstancedMesh
渲染大量具有相同几何体与材质、但具有不同世界变换的物体
// 创建200个正方体
let boxes = new THREE.InstancedMesh(
new THREE.BoxGeometry(0.1, 0.1, 0.1),
new THREE.MeshStandardMaterial({
color: 0xffffff,
emissive: 0xffffff,
emissiveIntensity: 10
}),
200,
)
const matrix = new THREE.Matrix4()
const color = new THREE.Color()
for (let i = 0; i < boxes.count; i++) {
matrix.setPosition(
Math.random() - 0.5,
Math.random() * 2,
Math.random() - 0.5,
)
boxes.setMatrixAt(i, matrix)
boxes.setColorAt(i, color.setHex(Math.random() * 0xffffff))
}
scene.add(boxes)
# 镜面反射 Reflector
let reflectorGeometry = new THREE.PlaneBufferGeometry(100,100);
let reflectorPlane = new Reflector(reflectorGeometry, {
textureWidth: window.innerWidth,
textureHeight: window.innerHeight,
color: 0x333333,
});
reflectorPlane.rotation.x = -Math.PI /2;
scene.add(reflectorPlane);
# 材质介绍
所有材质都会从父类Material继承一些属性和方法
// 透明设置
material.transparent = true;// 开启透明
material.opacity = 0.5;// 设置透明度
// 克隆 clone()
const mesh2 = mesh.clone();
// 克隆几何体和材质,重新设置mesh2的材质和几何体属性
mesh2.geometry = mesh.geometry.clone();
mesh2.material = mesh.material.clone();
// 改变mesh2颜色,不会改变mesh的颜色
mesh2.material.color.set(0xff0000);
// 复制 copy()
mesh.position.copy(mesh2.position);// 第1步位置重合
mesh.position.y += 100;// 第2步mesh在原来y的基础上增加100
// 设置多个材质
const plane = new THREE.Mesh(geometry, [material1, material2]);
# 网格材质 MeshMaterial
# 基础网格材质 MeshBasicMaterial
不会受到光照影响
// 从Material继承的任何属性都可以从此处传入
MeshBasicMaterial( parameters:Object )
# 漫反射网格材质 MeshLambertMaterial
会受到光照影响,该材质也可以称为Lambert网格材质
// 从Material继承的任何属性都可以从此处传入
MeshLambertMaterial( parameters:Object )
const material = new THREE.MeshLambertMaterial({
color: 0x00ffff,
wireframe:true,//线条模式渲染mesh对应的三角形数据
});
# 高光网格材质 MeshPhongMaterial
一种用于具有镜面高光的光泽表面的材质
// 从Material继承的任何属性都可以从此处传入
MeshPhongMaterial( parameters:Object )
const material = new THREE.MeshPhongMaterial({
color: 0xff0000,
shininess: 20, //高光部分的亮度,默认30
specular: 0x444444, // 高光部分的颜色
envMap: bgTexture,
refractionRatio: 0.7, // 空气的折射率
reflectivity: 0.99, // 环境贴图对表面的影响程度
});
# 点材质 PointsMaterial
// 从Material继承的任何属性都可以从此处传入
PointsMaterial( parameters:Object )
# 线材质 LineMaterial
# 基础线条材质 LineBasicMaterial
一种用于绘制线框样式几何体的材质
// 从Material继承的任何属性都可以从此处传入
LineBasicMaterial( parameters:Object )
// 线材质对象
const material = new THREE.LineBasicMaterial({
color: 0xff0000 //线条颜色
});
// 创建线模型对象
const line = new THREE.Line(geometry, material);
# 虚线材质 LineDashedMaterial
一种用于绘制虚线样式几何体的材质
// 从Material继承的任何属性都可以从此处传入
LineDashedMaterial( parameters:Object )
# PBR材质 physically-based rendering
基于物理的光照技术,可以提供更逼真的、更接近生活中的材质效果,当然也会占用更多的电脑硬件资源
# 标准网格材质 MeshStandardMaterial
一种基于物理的标准材质
// 从Material继承的任何属性都可以从此处传入
MeshStandardMaterial( parameters:Object )
new THREE.MeshStandardMaterial({
// 0.0到1.0之间的值可用于生锈的金属外观,默认是0.5
metalness: 1.0,// 金属度属性
// 0.0表示平滑的镜面反射,1.0表示完全漫反射,默认0.5
roughness: 0.5,// 表面粗糙度
})
# 物理网格材质 MeshPhysicalMaterial
MeshStandardMaterial的扩展,提供了更高级的基于物理的渲染属性
// 从Material继承的任何属性都可以从此处传入
MeshPhysicalMaterial( parameters:Object )
new THREE.MeshPhysicalMaterial( {
// 清漆层属性:可以用来模拟物体表面一层透明图层,就好比你在物体表面刷了一层透明清漆
clearcoat: 1.0, // 范围0到1。默认0
// 清漆层粗糙度属性:表示物体表面透明涂层.clearcoat对应的的粗糙度
clearcoatRoughness: 0.1,// 范围是为0.0至1.0。默认值为0.0
// 玻璃材质透光率
transmission: 1.0, // 范围是从0.0到1.0。默认值为0.0
// 折射率,不同材质的折射率,可以百度搜索
ior:1.5,// 非金属材料的折射率从1.0到2.333。默认值为1.5
});
# 精灵模型材质 SpriteMaterial
// 从Material继承的任何属性都可以从此处传入
SpriteMaterial( parameters:Object )
// 精灵模型设置颜色贴图.map
const texture = new THREE.TextureLoader().load("./光点.png");
const spriteMaterial = new THREE.SpriteMaterial({
color:0x00ffff,// 如果.map是纯白色贴图,可以设置为其他任意颜色
map: texture, // 设置精灵纹理贴图
transparent:true, // 如果贴图是背景透明的png贴图
blending: THREE.AdditiveBlending, // 如果png有半透明的需要设置
depthTest: true // 开启深度测试,显示正常的层级关系
});
把Canvas画布作为Sprite精灵模型的颜色贴图
// 创建一个canvas画布
const canvas = createCanvas('设备A');
// canvas画布作为CanvasTexture的参数创建一个纹理对象
const texture = new THREE.CanvasTexture(canvas);
const spriteMaterial = new THREE.SpriteMaterial({
map: texture,
});
const sprite = new THREE.Sprite(spriteMaterial)
# 贴图介绍
# 纹理贴图 material.map
纹理贴图map和color属性值会混合。如果设置了纹理贴图map不用设置color的值,color默认白色0xffffff
const geometry = new THREE.PlaneGeometry(200, 100);
//纹理贴图加载器TextureLoader
const texLoader = new THREE.TextureLoader();
// .load()方法异步加载图像,返回一个纹理对象Texture
const texture = texLoader.load('./earth.jpg', function () {
renderer.render(scene, camera);
})
// 设置纹理对象的颜色空间
texture.colorSpace = THREE.SRGBColorSpace
const material = new THREE.MeshLambertMaterial({
// 不用设置color的值
// color: 0x00ffff,
// 纹理对象Texture翻转属性.flipY默认值是true
texture.flipY = false;
// 设置纹理贴图:Texture对象作为材质map属性的属性值
map: texture,// map表示材质的颜色贴图属性
});
# 颜色空间 colorSpace
纹理对象Texture颜色空间(colorSpace)属性默认值是THREE.NoColorSpace
THREE.NoColorSpace = ""
THREE.SRGBColorSpace = "srgb" # SRGB颜色空间
THREE.LinearSRGBColorSpace = "srgb-linear" # 线性颜色空间
texture.colorSpace = THREE.SRGBColorSpace
# 阵列设置 wrapS/wrapT
纹理对象Texture的阵列设置
wrapS # 定义了纹理贴图在水平方向上排列方式,在UV映射中对应于U
wrapT # 定义了纹理贴图在垂直方向上排列方式,在UV映射中对应于V
THREE.ClampToEdgeWrapping # 默认,纹理边缘将被推到外部边缘的纹素
THREE.RepeatWrapping # 纹理将简单地重复到无穷大
THREE.MirroredRepeatWrapping # 纹理将重复到无穷大,在每次重复时将进行镜像
// 设置阵列模式
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
// uv两个方向纹理重复数量
texture.repeat.set(12,12);//注意选择合适的阵列数量
# 背景透明 transparent
把一个背景透明的.png图像作为平面矩形网格模型Mesh的颜色贴图
const material = new THREE.MeshBasicMaterial({
map: textureLoader.load('./compass.png'),
// 使用背景透明的png贴图,注意允许透明
transparent: true,
});
# 环境贴图 material.envMap
环境贴图就是一个模型周围环境的图像,对PBR材质渲染效果影响较大,一般用于渲染PBR材质的模型
# 对于PBR材质,即使不添加任何光源,只使用环境贴图,物体表面的颜色也能看到
# 这说明环境贴图其实相当于提供了物体周围环境发射或反射的光线
# 立方体纹理加载器 CubeTextureLoader
使用 load() 方法加载6张图片,返回一个立方体纹理对象 CubeTexture (父类是纹理对象Texture)
// 上下左右前后6张贴图构成一个立方体空间
// 'px.jpg', 'nx.jpg':x轴正方向、负方向贴图 p:正positive n:负negative
// 'py.jpg', 'ny.jpg':y轴贴图
// 'pz.jpg', 'nz.jpg':z轴贴图
const textureCube = new THREE.CubeTextureLoader().setPath('./环境贴图/')
.load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']);
// 通过PBR材质的贴图属性可以实现模型表面反射周围景物
new THREE.MeshStandardMaterial({
metalness: 1.0, // 金属度属性
roughness: 0.5, // 表面粗糙度
envMapIntensity: 1.0, // 环境贴图对模型表面的影响能力
envMap: textureCube, // 设置pbr材质环境贴图
})
如果希望环境贴图能影响场景中所有的Mesh,可以通过Scene的场景环境属性设置
scene.environment = textureCube;
纹理和渲染器颜色空间一致
//如果renderer.outputColorSpace=THREE.sRGBEncoding;环境贴图需要保持一致
cubeTexture.encoding = THREE.SRGBColorSpace;
# hdr格式的环境贴图 RGBELoader
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
const rgbeLoader = new RGBELoader();
rgbeLoader.load('./envMap.hdr', function (envMap) {
scene.environment = envMap;
// hdr作为环境贴图生效,设置.mapping为EquirectangularReflectionMapping,球形贴图
envMap.mapping = THREE.EquirectangularReflectionMapping;
})
# GUI可视化调试PBR材质属性
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
const gui = new GUI();
const matFolder = gui.addFolder('材质属性');
matFolder.add(mesh.material,'metalness',0,1);
matFolder.add(mesh.material,'roughness',0,1);
matFolder.add(mesh.material,'clearcoat',0,1);
matFolder.add(mesh.material,'clearcoatRoughness',0,1);
matFolder.add(mesh.material,'envMapIntensity',0,10);
matFolder.add(mesh.material,'transmission',0,1);
matFolder.add(mesh.material,'ior',0,3);
# 车外壳材质设置
const mesh = gltf.scene.getObjectByName('外壳01');
// 创建一个MeshPhysicalMaterial材质
mesh.material = new THREE.MeshPhysicalMaterial({
color: mesh.material.color, //默认颜色
// PBR材质设置
metalness: 0.9,// 车外壳金属度
roughness: 0.5,// 车外壳粗糙度
// 车外壳油漆效果
clearcoat: 1.0,// 物体表面清漆层或者说透明涂层的厚度
clearcoatRoughness: 0.1,// 透明涂层表面的粗糙度
envMap: textureCube, // 环境贴图
envMapIntensity: 2.5, // 环境贴图对Mesh表面影响程度
})
# 玻璃材质设置
const mesh = gltf.scene.getObjectByName('玻璃01')
mesh.material = new THREE.MeshPhysicalMaterial({
metalness: 0.0,//玻璃非金属
roughness: 0.0,//玻璃表面光滑
transmission: 1.0, //玻璃材质透光率
ior:1.5,//折射率
envMap:textureCube,//环境贴图
envMapIntensity: 1.0, //环境贴图对Mesh表面影响程度
})
# 其它贴图
# Canvas贴图 CanvasTexture
设置填充文字,作为纹理 CanvasTexture 引入到材质 MeshPhongMaterial 中
function addCanvas() {
var canvas = document.createElement("canvas");
canvas.width = 512;
canvas.height = 64;
var c = canvas.getContext('2d');
c.fillStyle = "#aaaaff";
c.fillRect(0, 0, 512, 64);
// 文字
c.beginPath();
c.translate(256, 32);
c.fillStyle = "#FF0000"; //文本填充颜色
c.font = "bold 28px 宋体"; //字体样式设置
c.textBaseline = "middle"; //文本与fillText定义的纵坐标
c.textAlign = "center"; //文本居中(以fillText定义的横坐标)
c.fillText("左本的博客,Three.js3D文字", 0, 0);
c.closePath();
var cubeGeometry = new THREE.BoxGeometry(512, 64, 5);
// CanvasTexture纹理
canvasTexture = new THREE.CanvasTexture(canvas);
canvasTexture.wrapS = THREE.RepeatWrapping;
var material = new THREE.MeshPhongMaterial({
map: canvasTexture, // 设置纹理贴图
});
var cube = new THREE.Mesh(cubeGeometry, material);
cube.rotation.y += Math.PI; //-逆时针旋转,+顺时针
scene.add(cube);
}
注意
如果Canvas包含外部图片作为背景,需要等图像加载完成再执行THREE.CanvasTexture(canvas)
const img = new Image();
img.src = "./标签箭头背景.png";
img.onload = function () {
const canvas = createCanvas(img,'设备A');//创建一个canvas画布
// 图片加载完成后,读取canvas像素数据创建CanvasTexture
const texture = new THREE.CanvasTexture(canvas);
}
# 视频贴图 VideoTexture
let video = document.createElement('video');
video.src = './assets/zp2.mp4';
video.loop = true;
video.muted = true;
let videoTexture = new THREE.VideoTexture(video);
const videoGeoplane = new THREE.PlaneBufferGeometry(16,9);
const videoMaterial = new THREE.MeshBasicMaterial({
transparent: true,
side: THREE.DoubleSide,
});
const videoMesh = new THREE.Mesh(videoGeoplane, videoMaterial);
videoMesh.position.set(0,1,2);
scene.add(videoMesh);
window.addEventListener("mousemove",(e)=>{
if(video.paused){
video.play();
videoMaterial.map = videoTexture;
videoMaterial.map.needsUpdate = true;
}
})
# 水面贴图 waterNormals
import { Water } from 'three/examples/jsm/objects/Water'
// 创建水面几何体
const waterGeometry = new THREE.PlaneGeometry(10000, 10000);
// 设置水面材质
const water = new Water(waterGeometry, {
textureWidth: 512,
textureHeight: 512,
waterNormals: new THREE.TextureLoader()
.load('./img/threejs_waternormals.jpg',
function (texture) {
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
}
),
flowDirection: new THREE.Vector2(2, 1),
// 控制水面的光照效果和颜色
sunDirection: new THREE.Vector3(),
sunColor: 0xffffff,
waterColor: 0x001e0f,
distortionScale: 3.7,
fog: scene.fog !== undefined,
});
// 设置水面旋转
water.rotation.x = -Math.PI / 2;
scene.add(water);
// 水面效果需要随着时间变化而动态更新
function animate() {
requestAnimationFrame(animate);
water.material.uniforms['time'].value += 1.0 / 60.0; // 每帧更新时间参数
}
animate();
# 位移贴图 material.displacementMap
displacementScale属性表示位移贴图对网格的影响程度
displacementScale # 位移贴图对网格的影响程度(黑色是无位移,白色是最大位移)默认值为1
# 粗糙度贴图 material.roughnessMap
用于改变材质的粗糙度
roughness:材质的粗糙程度。0.0表示平滑的镜面反射,1.0表示完全漫反射
# 法线贴图 material.normalMap
让细节程度较低的表面生成高细节程度的精确光照方向和反射效果
normalMapType # 法线贴图的类型:THREE.TangentSpaceNormalMap(默认)和THREE.ObjectSpaceNormalMap
normalScale # 法线贴图对材质的影响程度。典型范围是0-1
var geometry = new THREE.BoxGeometry(100, 100, 100);
// 加载颜色纹理贴图
var texture = textureLoader.load('./img/normal.jpg');
var material = new THREE.MeshPhongMaterial({
color: 0xff0000,
normalMap: texture, // 法线贴图
normalScale: new THREE.Vector2(3, 3), // 设置深浅程度,默认值(1,1)
});
var mesh1 = new THREE.Mesh(geometry1, material1);
mesh1.position.set(150, 0, 0);
scene.add(mesh1);
# 凹凸贴图 material.bumpMap
黑色和白色值映射到与光照相关的感知深度。凹凸实际上不会影响对象的几何形状,只影响光照。如果定义了法线贴图,则将忽略该贴图
bumpScale # 凹凸贴图会对材质产生多大影响。范围是0-1。默认值为1
var geometry = new THREE.BoxGeometry(100, 100, 100);
var textureLoader = new THREE.TextureLoader();
// 加载颜色纹理贴图
var texture = textureLoader.load('./img/墙1.jpg');
// 加载凹凸贴图
var textureBump = textureLoader.load('./img/墙.jpg');
var material = new THREE.MeshPhongMaterial({
map: texture,// 普通纹理贴图
bumpMap:textureBump,// 凹凸贴图
bumpScale:3,// 设置凹凸高度,默认值1
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
# 环境光遮蔽贴图 material.aoMap
用来描绘物体和物体相交或靠近的时候遮挡周围漫反射光线的效果,如井盖边沿的缝隙
aoMapIntensity # 环境遮挡效果的强度。默认值为1。零是不遮挡
注意
- MeshLambertMaterial、MeshBasicMaterial 没有凹凸、法线贴图属性
- 只设置环境光的情况下,没有办法查看到法线贴图和凹凸贴图的效果
# 光源介绍
# 环境光 AmbientLight
环境光没有特定方向,不会产生阴影,只是整体改变场景的光照明暗
// 参数1:一个表示颜色的 Color 的实例、字符串或数字,默认为一个白色
// 参数2:光照强度。默认值为 1
const ambient = new THREE.AmbientLight(0xffffff, 5);
scene.add(ambient);
# 点光源 PointLight
可以类比为一个发光点,就像生活中一个灯泡以灯泡为中心向四周发射光线
// color(可选)一个表示颜色的 Color 的实例、字符串或数字,默认值为 0xffffff
// intensity(可选)光照强度。默认值为 1
// distance(可选)光源照射的最大距离。默认值为 0(无限远)
// decay(可选)沿着光照距离的衰退量。默认值为 2
PointLight( color:Color, intensity:Float, distance:Number, decay:Float )
// 也可以通过光照属性设置
pointLight.intensity = 1.0;
// 也可以设置照射目标
pointLight.target = mesh;
//点光源位置
pointLight.position.set(400, 0, 0);//点光源放在x轴上
scene.add(pointLight); //点光源添加到场景中
点光源辅助观察 PointLightHelper
// 参数1:要模拟的光源
// 参数2:点光源球形辅助对象的尺寸。默认为 1
const pointLightHelper = new THREE.PointLightHelper(pointLight, 10);
scene.add(pointLightHelper);
# 平行光 DirectionalLight
这种光的表现像是无限远,从它发出的光线都是平行的。常常用平行光来模拟太阳光的效果
// 参数1:一个表示颜色的 Color 的实例、字符串或数字,默认为一个白色
// 参数2:光照强度。默认值为 1
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(80, 100, 50);
// 平行光指向对象网格模型mesh,可以不设置,默认的位置是0,0,0
// 如果要改为除默认值之外的其他位置,该位置必须被添加到场景(scene)中去
directionalLight.target = mesh;
scene.add(directionalLight);
平行光辅助观察 DirectionalLightHelper
// 参数1:要模拟的光源
// 参数2:平面的尺寸。默认为 1
// 参数3:如果没有设置颜色将使用光源的颜色
const dirLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5, 0xff0000);
scene.add(dirLightHelper);
# 聚光灯光源 SpotLight
光线从一个点沿一个方向射出,随着光线照射的变远,光线圆锥体的尺寸也逐渐增大
// color(可选)一个表示颜色的 Color 的实例、字符串或数字,默认为白色(0xffffff)
// intensity(可选)光照强度。默认值为 1
// distance 光源照射的最大距离。默认值为 0(无限远)
// angle 光线照射范围的角度。默认值为 Math.PI/3
// penumbra 聚光锥的半影衰减百分比。默认值为 0
// decay 沿着光照距离的衰减量。默认值为 2
SpotLight(color:Color,intensity:Float,distance:Float,angle:Radians,penumbra:Float,decay:Float)
聚光源目标对象.target和光源的位置.position共同确定聚光源照射方向
// 设置聚光光源位置
spotLight.position.set(0, 50, 0);
// 目标对象是一个模型对象Object3D,默认在坐标原点
spotLight.target.position.set(50,0,0);
// 目标对象添加到场景中.target.position才会起作用
scene.add(spotLight.target);
聚光灯辅助观察 SpotLightHelper
const spotLight = new THREE.SpotLight( 0xffffff )
spotLight.position.set( 10, 10, 10 )
scene.add( spotLight )
// light:被模拟的光源
// color:(可选) 如果没有赋值辅助对象将使用光源的颜色
const spotLightHelper = new THREE.SpotLightHelper( spotLight )
scene.add( spotLightHelper )
# 半球光 HemisphereLight
光源直接放置于场景之上,光照颜色从天空光线颜色渐变到地面光线颜色,半球光不能投射阴影
// skyColor(可选)一个表示颜色的 Color 的实例、字符串或数字,默认为白色(0xffffff)
// groundColor(可选)一个表示颜色的 Color 的实例、字符串或数字,默认为白色(0xffffff)
// intensity(可选)光照强度。默认值为 1
const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 )
scene.add(light)
半球光辅助观察 HemisphereLightHelper
const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 )
// light:被模拟的光源
// size:用于模拟光源的网格尺寸
// color:(可选的) 如果没有赋值辅助对象将使用光源的颜色
const helper = new THREE.HemisphereLightHelper( light, 5 )
scene.add( helper )
# 平面光光源 RectAreaLight
平面光光源从一个矩形平面上均匀地发射光线。这种光源可以用来模拟像明亮的窗户或者条状灯光光源
- 不支持阴影,只有点光源、平行光光源、聚光灯光源有阴影
- 只支持 MeshStandardMaterial 和 MeshPhysicalMaterial 两种材质
- 你必须在你的场景中加入 RectAreaLightUniformsLib,并调用 init()
// color(可选)一个表示颜色的 Color 的实例、字符串或数字,默认为白色(0xffffff)
// intensity(可选)光源强度/亮度 。默认值为 1
// width(可选)光源宽度。默认值为 10
// height(可选)光源高度。默认值为 10
RectAreaLight( color:Color, intensity:Float, width:Float, height:Float )
平面光辅助观察 RectAreaLightHelper
import { RectAreaLightHelper } from 'three/examples/jsm/helpers/RectAreaLightHelper.js'
const light = new THREE.RectAreaLight( 0xffffbb, 1.0, 5, 5 )
// light:被模拟的光源
// color:(可选) 如果没有赋值辅助对象将使用光源的颜色
const helper = new RectAreaLightHelper( light )
scene.add( helper )
# 平行光阴影 DirectionalLightShadow
// 1、光源阴影投射属性.castShadow
directionalLight.castShadow = true;
// 2、模型阴影投射.castShadow,接收属性.receiveShadow
mesh.castShadow = true;
mesh.receiveShadow = true;
// 3、模型阴影接收属性.receiveShadow
planeMesh.receiveShadow = true;
// 4、允许渲染器渲染阴影 .shadowMap
renderer.shadowMap.enabled = true;
// THREE.BasicShadowMap:未过滤,速度最快,但质量最差
// THREE.PCFShadowMap (默认):使用 PCF 算法来过滤阴影映射
// THREE.PCFSoftShadowMap:使用 PCF 算法来过滤阴影映射,但在使用低分辨率阴影图时具有更好的软阴影
// THREE.VSMShadowMap:使用 VSM 算法来过滤阴影映射,所有阴影接收者也将会投射阴影
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// 5、可视化.shadow.camera
const cameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
scene.add(cameraHelper);
// 6、设置阴影渲染范围.shadow.camera
// 平行光阴影相机属性.shadow.camera的属性值是一个正投影相机对象OrthographicCamera,属性相同
directionalLight.shadow.camera.left = -50;
directionalLight.shadow.camera.right = 50;
directionalLight.shadow.camera.top = 200;
directionalLight.shadow.camera.bottom = -100;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 600;
// 配置阴影的分辨率,值越大阴影质量越好
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
// 7、阴影贴图尺寸(提升边缘渲染效果).shadow.mapSize,默认512x512
directionalLight.shadow.mapSize.set(128,128)
// 8、弱化模糊阴影边缘.shadow.radius
directionalLight.shadow.radius = 3;
注意
directionalLight.shadow.camera.far
一定要大于 directionalLight.position
的长度
实际生活中所有物体都可以产生阴影,同时所有物体都可以接收其它物体的阴影
gltf.scene.traverse(function (obj) {
if (obj.isMesh) {
// 批量设置所有Mesh都可以产生阴影和接收阴影
obj.castShadow = true;
obj.receiveShadow = true;
}
});
# 镜头光晕 Lensflare
// texture:用于光晕的THREE.Texture(贴图)
// size:(可选)光晕尺寸(单位为像素)
// distance:(可选)和光源的距离值在0到1之间(值为0时在光源的位置)
// color:(可选)光晕的(Color)颜色
LensflareElement( texture:Texture, size:Float, distance:Float, color:Color )
import { Lensflare, LensflareElement } from 'three/examples/jsm/objects/Lensflare.js';
const light = new THREE.PointLight( 0xffffff, 1.5, 2000 );
const textureLoader = new THREE.TextureLoader();
const textureFlare0 = textureLoader.load( "textures/lensflare/lensflare0.png" );
const textureFlare1 = textureLoader.load( "textures/lensflare/lensflare2.png" );
const textureFlare2 = textureLoader.load( "textures/lensflare/lensflare3.png" );
const lensflare = new Lensflare();
lensflare.addElement( new LensflareElement( textureFlare0, 512, 0 ) );
lensflare.addElement( new LensflareElement( textureFlare1, 512, 0 ) );
lensflare.addElement( new LensflareElement( textureFlare2, 60, 0.6 ) );
light.add( lensflare );
# 层级模型
场景 scene 是 group 的父对象,group 是 mesh1、mesh2 的父对象。这样就构成了一个三层的层级结构
//创建两个网格模型mesh1、mesh2
const geometry = new THREE.BoxGeometry(20, 20, 20);
const material = new THREE.MeshLambertMaterial({color: 0x00ffff});
const mesh1 = new THREE.Mesh(geometry, material);
const mesh2 = new THREE.Mesh(geometry, material);
mesh2.translateX(25);
// 创建层级模型
const group = new THREE.Group();
// 把mesh1型插入到组group中,mesh1作为group的子对象
group.add(mesh1);
// 把mesh2型插入到组group中,mesh2作为group的子对象
group.add(mesh2);
// 把group插入到场景中作为场景子对象
scene.add(group);
// 查看子对象.children
console.log('查看Scene的子对象',scene.children);
console.log('查看group的子对象',group.children);
如果父对象group进行旋转、缩放、平移变换,子对象同样跟着变换
// 沿着Y轴平移mesh1和mesh2的父对象,mesh1和mesh2跟着平移
group.translateY(100);
// 父对象缩放,子对象跟着缩放
group.scale.set(4,4,4);
// 父对象旋转,子对象跟着旋转
group.rotateY(Math.PI/6);
场景对象Scene、组对象Group的.add()方法都是继承自它们共同的基类(父类)Object3D
// 可以单独插入一个对象,也可以同时插入多个子对象
group.add(mesh1,mesh2);
// Object3D作为Group来使用
const mesh1 = new THREE.Mesh(geometry, material);
const mesh2 = new THREE.Mesh(geometry, material);
const obj = new THREE.Object3D();// 作为mesh1和mesh2的父对象
obj.add(mesh1,mesh2);
// mesh也可以添加子对象,mesh基类也是Object3D
mesh1.add(mesh2);
# 移除对象 .remove()
// 移除父对象group的子对象网格模型mesh1
group.remove(mesh1);
// 一次移除多个子对象
group.remove(mesh1,mesh2);
# 模型隐藏或显示 .visible
group.visible =false;// 隐藏一个包含多个模型的组对象group
// 隐藏网格模型mesh,visible的默认值是true
// 注意如果mesh2和mesh的.material属性指向同一个材质,mesh2也会跟着mesh隐藏
mesh.material.visible =false;
# 模型命名 .name
const group1 = new THREE.Group();
group1.name='东区房子';
const mesh1 = new THREE.Mesh(geometry, material);
mesh1.name='一号楼';
group1.add(mesh1);
const group2 = new THREE.Group();
group2.name='西区房子';
const mesh2 = new THREE.Mesh(geometry, material);
mesh2.name='二号楼';
group2.add(mesh2);
const model = new THREE.Group();
model.name='小区房子';
model.add(group1, group2);
# 递归遍历方法 .traverse()
// 递归遍历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);
# 本地坐标和世界坐标
- 任何一个模型的本地坐标(局部坐标)就是模型的.position属性
- 任何一个模型的世界坐标,就是模型自身.position和所有父对象.position累加的坐标
获取世界坐标 .getWorldPosition()
// 声明一个三维向量用来表示某个坐标
const worldPosition = new THREE.Vector3();
// 获取mesh的世界坐标
mesh.getWorldPosition(worldPosition);
console.log('世界坐标',worldPosition);
console.log('本地坐标',mesh.position);
给子对象添加一个局部坐标系
//可视化mesh的局部坐标系
const meshAxesHelper = new THREE.AxesHelper(50);
mesh.add(meshAxesHelper);
更新物体的全局变换
model.add(mesh1, mesh2, mesh3);
model.updateMatrix();
model.updateMatrixWorld();
# GUI的使用
轻量级的图形用户界面库,可以很容易地创建出能够改变代码变量的界面组件
// 引入dat.gui.js的一个类GUI
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
//创建GUI对象
const gui = new GUI();
# add方法
// 通过GUI改变环境光强度属性.intensity
gui.add(ambient, 'intensity', 0, 2.0);
// 通过GUI改变改变物体位置
gui.add(mesh.position, 'x', 0, 180);
gui.add(mesh.position, 'y', 0, 180);
gui.add(mesh.position, 'z', 0, 180);
// 下拉菜单
const obj = {
scale: 0,
};
// 数组(下拉菜单)
gui.add(obj, 'scale', [-100, 0, 100]).name('y坐标').onChange(function (value) {
mesh.position.y = value;
});
// 对象(下拉菜单)
gui.add(obj, 'scale', {
left: -100,
center: 0,
right: 100
// 左: -100,//可以用中文
// 中: 0,
// 右: 100
}).name('位置选择').onChange(function (value) {
mesh.position.x = value;
});
// 单选框
const obj = {
bool: false,
};
// 改变的obj属性数据类型是布尔值,交互界面是单选框
gui.add(obj, 'bool').name('是否旋转');
# addColor方法
生成颜色值改变的交互界面
const obj = {
color:0x00ffff,
};
// .addColor()生成颜色值改变的交互界面
gui.addColor(obj, 'color').onChange(function(value){
mesh.material.color.set(value);
});
# name方法
改变gui生成交互界面显示的内容
gui.add(ambient, 'intensity', 0, 2.0).name('环境光强度');
gui.add(directionalLight, 'intensity', 0, 2.0).name('平行光强度');
# step方法
可以设置交互界面每次改变属性值间隔是多少
gui.add(ambient, 'intensity', 0, 2.0).name('环境光强度').step(0.1);
# onChange方法
const obj = {
x: 30,
};
// 当obj的x属性变化的时候,就把此时obj.x的值value赋值给mesh的x坐标
gui.add(obj, 'x', 0, 180).onChange(function(value){
mesh.position.x = value;
// 你可以写任何你想跟着obj.x同步变化的代码
// 比如mesh.position.y = value;
});
# addFolder方法
可以创建一个子菜单,进行分组
// 环境光子菜单
const ambientFolder = gui.addFolder('环境光');
dirFolder.open();//打开菜单
ambientFolder.add(ambient, 'intensity',0,2);
// 平行光子菜单
const dirFolder = gui.addFolder('平行光');
dirFolder.close();//关闭菜单
dirFolder.add(directionalLight, 'intensity',0,2);
// 子菜单嵌套子菜单
const dirFolder2 = dirFolder.addFolder('位置');//子菜单的子菜单
dirFolder2.close();//关闭菜单
dirFolder2.add(directionalLight.position, 'x',-400,400);
dirFolder2.add(directionalLight.position, 'y',-400,400);
dirFolder2.add(directionalLight.position, 'z',-400,400);
# 三维包围盒
包围盒Box3表示三维长方体所包围的区域,参数min和max属性值都是三维向量Vector3
# 包围盒需要通过xyz坐标来表示,X范围[Xmin,Xmax],Y范围[Ymin,Ymax],Z范围[Zmin,Zmax]
min属性值是Vector3(Xmin, Ymin, Zmin)
max属性值是Vector3(Xmax, Ymax, Zmax)
const box = new THREE.Box3()
box.min = new THREE.Vector3(-10, -10, 0);
box.max = new THREE.Vector3(10, 10, 10);
# 常用方法
computeBoundingBox():计算模型包围盒
let duckMesh = gltf.scene.getobjectByName("LoD3spShape");
let duckGeometry = duckMesh.geometry;
// 计算包围盒
duckGeometry.computeBoundingBox();
// 获取包围盒
let duckBox = duckGeometry.boundingBox;
如果模型被缩小或放大了,但是外层模型没变,需要更新世界矩阵
// 更新世界矩阵
duckMesh.updateWorldMatrix(true, true);
// 更新包围盒
duckBox.applyMatrix4(duckMesh.matrixworld);
expandByObject():计算层级模型group包围盒,比如一个Group对象有多个网格模型Mesh作为子对象
var loader = new THREE.ObjectLoader();
loader.load('group.json', function(group) {
scene.add(group);// 加载返回的模型对象插入场景
var box3 = new THREE.Box3()
// 计算层级模型group包围盒
box3.expandByObject(group)
console.log('查看包围盒box3', box3);
})
expandByScalar():包围盒整体尺寸放大
// 缩放包围盒,尺寸放大1.5倍
box3.expandByScalar(1.5)
getSize():返回包围盒具体的长宽高尺寸
const scale = new THREE.Vector3()
// 获得包围盒长宽高尺寸,结果保存在参数三维向量对象scale中
box3.getSize(scale)
console.log('模型包围盒尺寸', scale);
getCenter():返回包围盒几何中心
const center = new THREE.Vector3()
box3.getCenter(center)
console.log('模型中心坐标', center);
union():获取多个物体包围盒
var box = new THREE.Box3();
for (let i =0; i<scene.children.length; i++){
// 获取当前物体的包围盒
scene.children[i].geometry.computeBoundingBox();
// 获取包围盒
let box3 = scene.children[i].geometry.boundingBox;
// 合并包围盒
box.union(box3);
//第二种方式
let box3 = neW THREE.Box3().setFromobject(arrSphere[i]);
//合并包围剂
box.union(box3);
}
getBoundingSphere():获取包围球
var sphere = new THREE.Sphere()
// 包围盒Box3和包围球Sphere可以相互等价转化,通过包围盒对象来计算包围球对象
box3.getBoundingSphere(sphere)
# 辅助查看对象
三维包围盒的辅助查看对象
// 参数1:包围盒,参数2:(可选的) 线框盒子的颜色,默认为 0xffff00
const helper = new THREE.Box3Helper( box, 0xffff00 );
scene.add( helper );
三维包围球的辅助查看对象
// 获取包围球
let duckSphere = duckGeometry.boundingSphere;
duckSphere.applyMatrix4(duckMesh.matrixworld);
// 创建包围球辅助器
let sphereGeometry = new THREE.SphereGeometry(duckSphere.radius,16,16);
# 加载文件
常见3D模型文件格式:
gltf # JSON格式,已成为Web端标准。可以包含所有的三维模型相关信息的数据
# 比如模型层级关系、PBR材质、纹理贴图、骨骼动画、变形动画
# 有些glTF文件会关联一个或多个.bin文件,.bin文件以二进制形式存储了模型的顶点数据等信息
glb # 是gltf的二进制文件,可以把.gltf模型和贴图信息全部合成得到一个glb文件中
# glb文件相对gltf文件体积更小,网络传输自然更快
fbx # Autodesk FBX,闭源格式。支持3D模型、场景层次、材质照明、动画、骨骼、蒙皮、及混合形状
obj # 适用于简单的静态模型。支持3D模型、场景层次、材质照明、动画、骨骼、蒙皮、及混合形状
# 加载glb、gltf模型
- 单独.gltf文件,单独.glb文件,.gltf + .bin + 贴图文件,这三种不同形式的,都使用GLTFLoader加载
- .gltf + .bin + 贴图文件,这种以单独文件形式存在的,注意不要随意改变子文件相对父文件gltf的目录
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
loadGlbModel() {
const loader = new GLTFLoader()
loader.load(`${this.publicPath}model/12OJJ6MOWT722N61Z5N92KA9C.glb`, gltf => {
gltf.scene.scale.set(100,100,100)
gltf.scene.position.set(0,0,0)
let axis = new THREE.Vector3(0,1,0);//向量axis
gltf.scene.rotateOnAxis(axis,Math.PI/2);
//绕axis轴逆旋转π/16
gltf.scene.rotateOnAxis(axis,Math.PI/-20);
gltf.scene.rotateOnAxis(axis,Math.PI/50);
// .load()方法加载图像,返回一个纹理对象Texture
const texLoader = new THREE.TextureLoader()
const texture = texLoader.load('earth.jpg', function () {
renderer.render(scene, camera)
})
// 纹理贴图的颜色空间
texture.colorSpace = THREE.SRGBColorSpace
// 纹理对象Texture翻转属性.flipY默认值是true
texture.flipY = false;
// 网格模型Mesh更换颜色贴图
gltf.scene.getObjectByName('Mesh077').material.map = texture
// 计算模型的包围盒
const box = new THREE.Box3().setFromObject(gltf.scene);
const center = box.getCenter(new THREE.Vector3());
// 调整模型的位置以将其移动到中心点
gltf.scene.position.sub(center);
// 返回的场景对象gltf.scene插入到threejs场景中
scene.add(gltf.scene)
// 需要渲染一下场景
renderer.render(scene, camera);
}, (xhr) => {
console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
}, (error) => {
console.error(error)
})
}
注意
- 给gltf模型更换贴图时,需要给更换的Mesh模型添加UV贴图,即使是空的
- 可以使用:
loader.loadAsync()
方法,异步加载
# 加载FBX模型
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
loadFbxModel() {
const loader = new FBXLoader();
loader.load(`${this.publicPath}model/glbxz.com6031.FBX`, object => {
// 递归遍历批量修改FBX所有Mesh的信息
object.traverse( child => {
if ( child.isMesh ){
child.castShadow = true;
child.receiveShadow = true;
}
});
// 插入到threejs场景中
this.scene.add(object);
// 需要渲染一下场景
renderer.render(scene, camera);
})
}
# 加载obj模型和材质
const mtlLoader = new MTLLoader()
mtlLoader.load('./file/office.mtl', function (materials) {
console.log(materials)
materials.preload()
const objLoader = new OBJLoader()
objLoader.setMaterials(materials)
objLoader.load('./file/office.obj', function (obj) {
console.log(obj)
// obj.scale.set(1, 1, 1)
t.scene.add(obj)
})
})
通过上面方式分别加载模型和材质,如果看到的效果和实际有差异,建议在ThreeJS中单独设置材质
export const getFrame = async function():Promise<Mesh| null>{
const frameColorTexture = textureLoader.load('/WoodFloor024_1K_Color.jpg')
const frameRoughnessTexture = textureLoader.load('/WoodFloor024_1K_Roughness.jpg')
const frameDispTexture = textureLoader.load('/WoodFloor024_1K_Displacement.jpg')
const group = await objLoader.loadAsync('/frame.obj')
if(group instanceof Group){
const frame:Mesh = group.children[0] as Mesh
(frame.material as Material).dispose() // 清除自带的材质
frame.material = new MeshStandardMaterial({
map: frameColorTexture, // 普通贴图
roughnessMap: frameRoughnessTexture, // 粗糙贴图
bumpMap: frameDispTexture // 凹凸贴图
})
frame.position.y = 45
frame.position.y = -1
frame.rotation.y = Math.PI / 180 * -90
frame.scale.set(2,2,2)
return frame
} else {
return null
}
}
// 加载到场景
getFrame().then(frame => {
frame && scene.add(frame)
})
# 加载draco压缩后的模型
- 通过Draco进行压缩
// 全局安装
npm install -g gltf-pipeline
// Converting a glTF to glb
const gltfPipeline = require("gltf-pipeline");
const fsExtra = require("fs-extra");
const gltfToGlb = gltfPipeline.gltfToGlb;
const gltf = fsExtra.readJsonSync("./input/model.gltf");
const options = { resourceDirectory: "./input/" };
gltfToGlb(gltf, options).then(function (results) {
fsExtra.writeFileSync("model.glb", results.glb);
});
// Converting a glb to embedded glTF
const gltfPipeline = require("gltf-pipeline");
const fsExtra = require("fs-extra");
const glbToGltf = gltfPipeline.glbToGltf;
const glb = fsExtra.readFileSync("model.glb");
glbToGltf(glb).then(function (results) {
fsExtra.writeJsonSync("model.gltf", results.gltf);
});
// Converting a glTF to Draco glTF
const gltfPipeline = require("gltf-pipeline");
const fsExtra = require("fs-extra");
const processGltf = gltfPipeline.processGltf;
const gltf = fsExtra.readJsonSync("model.gltf");
const options = {
dracoOptions: {
compressionLevel: 10,
},
};
processGltf(gltf, options).then(function (results) {
fsExtra.writeJsonSync("model-draco.gltf", results.gltf);
});
// Saving separate textures
const gltfPipeline = require("gltf-pipeline");
const fsExtra = require("fs-extra");
const processGltf = gltfPipeline.processGltf;
const gltf = fsExtra.readJsonSync("model.gltf");
const options = {
separateTextures: true,
};
processGltf(gltf, options).then(function (results) {
fsExtra.writeJsonSync("model-separate.gltf", results.gltf);
// Save separate resources
const separateResources = results.separateResources;
for (const relativePath in separateResources) {
if (separateResources.hasOwnProperty(relativePath)) {
const resource = separateResources[relativePath];
fsExtra.writeFileSync(relativePath, resource);
}
}
});
- 也可以通过Blender压缩
- 在顶部引入'DRACOLoader'
import * as THREE from '../build/three.module.js';
import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from './jsm/loaders/DRACOLoader.js';
- 在threejs中进行加载,在draco文件中找到draco_decoder.js
// 创建加载器
const gltfLoader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
// 在draco文件中找到/draco/gltf/draco_decoder.js这个文件
//如果是vue直接放在项目的public下的'./draco/gltf/'目录即可
//这个路径主要是放draco的一些js文件的
dracoLoader.setDecoderPath('./draco/gltf/'); //这个路径是放draco_decoder.js这个文件的
dracoLoader.setDecoderConfig({ type: 'js' });
dracoLoader.preload();
gltfLoader.setDRACOLoader(dracoLoader);
// 然后直接加载模型即可
gltfLoader.load('./model/zhuji08-draco.glb',function(gltf){
scene.add(group)
})
# 加载json格式的模型
const loader = new THREE.ObjectLoader(); // 系统内部加载器
loader.load('上海外滩.json',function(data){
var buildGroup = new THREE.Group();
data.features.forEach(build => {
if(build.geometry){
if(build.geometry.type === 'Polygon'){
// 将"Polygon"和"MultiPolygon"的geometry.coordinates数据结构处理为一致
build.geometry.coordinates = [build.geometry.coordinates];
}
// build.properties.Floor*3近似表示楼的高度
var height = xy2lon(build.properties.Floor * 3);
buildGroup.add(ExtrudeMesh(build.geometry.coordinates,height))
}
});
model.add(buildGroup);
})
function xy2lon(height){
// 20037508.34对应地球经度的180度
var newHeight = height/20037508.34 * 180;
// 返回投影坐标
return newHeight;
}
function ExtrudeMesh(pointsArrs,height){
var shapeArr = [];
pointsArrs.forEach(pointsArr => {
var vector2Arr = [];
pointsArr[0].forEach(elem => {
vector2Arr.push(new THREE.Vector2(elem[0],elem[1]));
})
var shape = new THREE.Shape(vector2Arr);
shapeArr.push(shape)
});
var geometry = new THREE.ExtrudeGeometry(
shapeArr,
{
depth: height, // 拉伸高度
bevelEnabled: false // 无倒角
});
var mesh = new THREE.Mesh(geometry,material);
return mesh;
}
# 加载字体
字体转JSON文件 (opens new window) 可以使用 FontLoader 加载字体,并将字体对象赋给 TextGeometry 的font属性
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js';
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js';
function add3DFont() {
new FontLoader().load('font/FZYaoTi_Regular.json', function(font) {
//加入立体文字
var text = new TextGeometry("左本的博客,Three.js3D文字", {
// 设定文字字体
font: font,
//尺寸
size: 24,
//厚度
height: 5
});
text.computeBoundingBox();
// 设置偏移
text.translate(-220, 0, 0);
//3D文字材质
var m = new THREE.MeshStandardMaterial({
color: "#FF0000"
});
fontMesh = new THREE.Mesh(text, m)
fontMesh.position.y = 100;
scene.add(fontMesh);
});
}
# 外部模型材质是否共享的问题
由于楼房的Mesh共享了1号楼Mesh的材质,当你通过mesh1.material改变mesh1材质,本质上是改变所有楼Mesh的材质
const mesh1 = gltf.scene.getObjectByName("1号楼");
//1. 改变1号楼Mesh材质颜色
mesh1.material.color.set(0xff0000);
如果单独改变一个模型的材质,比如颜色,下面两个方案,可以任选其一:
- 三维建模软件中设置,需要代码改变材质的Mesh不要共享材质,要独享材质
- 代码批量更改:克隆材质对象,重新赋值给mesh的材质属性
//用代码方式解决mesh共享材质问题
gltf.scene.getObjectByName("小区房子").traverse(function (obj) {
if (obj.isMesh) {
// .material.clone()返回一个新材质对象,和原来一样,重新赋值给.material属性
obj.material = obj.material.clone();
}
});
mesh1.material.color.set(0xffff00);
mesh2.material.color.set(0x00ff00);
# 后期处理
所谓threejs后期处理,就像ps一样,对threejs的渲染结果进行后期处理,比如添加发光效果
# 不同功能后处理通道
查看threejs文件包目录 examples/jsm/postprocessing,可以看到threejs提供了很多后处理通道,想实现什么样的后期处理效果,需要调用threejs对应的后处理通道扩展库
OutlinePass.js # 高亮发光描边
UnrealBloomPass.js # Bloom发光
GlitchPass.js # 画面抖动效果
# 渲染器通道 RenderPass
// 引入后处理扩展库EffectComposer.js
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
// 需要对一个webgl渲染器的渲染结果进行后期处理,就把它作为EffectComposer的参数
const composer = new EffectComposer(renderer);
// 引入渲染器通道RenderPass
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
// 创建一个渲染器通道,场景和相机作为参数
const renderPass = new RenderPass(scene, camera);
// 给EffectComposer添加一个渲染器通道renderPass
composer.addPass(renderPass);
# 高亮发光描边 OutlinePass
- OutlinePass通道
// 引入OutlinePass通道
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js';
// 创建OutlinePass通道
const v2 = new THREE.Vector2(window.innerWidth, window.innerHeight);
// const v2 = new THREE.Vector2(800, 600);
// OutlinePass第一个参数v2的尺寸和canvas画布保持一致
const outlinePass = new OutlinePass(v2, scene, camera);
// 场景中有多个模型的话,就可以通过 selectedObjects 属性设置给哪个模型对象设置发光描边效果
outlinePass.selectedObjects = [mesh];
// 多个模型对象
outlinePass.selectedObjects = [mesh1,mesh2,group];
// 把创建好的OutlinePass通道添加到后处理composer中
composer.addPass(outlinePass);
- OutlinePass描边样式
// 模型描边颜色,默认白色
outlinePass.visibleEdgeColor.set(0xffff00);
// 高亮发光描边厚度,默认值1
outlinePass.edgeThickness = 5;
// 高亮描边发光强度,默认值3,0-100,设置太大会看不出描边的颜色
outlinePass.edgeStrength = 20;
// 模型闪烁频率,默认0不闪烁
outlinePass.pulsePeriod = 2;
- 渲染循环执行EffectComposer.render()
function render() {
composer.render();
// 渲染循环中后处理EffectComposer执行.render(),会调用webgl渲染器执行.render()
// renderer.render(scene, camera); // 不用再执行
requestAnimationFrame(render);
}
render();
# Bloom发光 UnrealBloomPass
针对场景中所有白色的物体
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
// 创建UnrealBloomPass通道
const v2 = new THREE.Vector2(window.innerWidth, window.innerHeight);
// const v2 = new THREE.Vector2(800, 600);
// 第二个参数发光强度,第二个参数半径,第二个参数起始点
const bloomPass = new UnrealBloomPass(v2, 1.5, 0.4, 0.85);
// 把创建好的UnrealBloomPass通道添加到后处理composer中
composer.addPass(bloomPass);
// 也可以再调整bloom发光强度
bloomPass.strength = 2.0;
function render() {
composer.render();
// 渲染循环中后处理EffectComposer执行.render(),会调用webgl渲染器执行.render()
// renderer.render(scene, camera); // 不用再执行
requestAnimationFrame(render);
}
render();
# 画面抖动效果 GlitchPass
针对场景的画面抖动,类似恐怖片
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js';
// 创建GlitchPass通道
const glitchPass = new GlitchPass();
// 把创建好的UnrealBloomPass通道添加到后处理composer中
composer.addPass(glitchPass);
function render() {
composer.render();
// 渲染循环中后处理EffectComposer执行.render(),会调用webgl渲染器执行.render()
// renderer.render(scene, camera); // 不用再执行
requestAnimationFrame(render);
}
render();
# 处理颜色异常(伽马校正)
如果使用了后处理功能EffectComposer,renderer.outputColorSpace会无效出现颜色偏差
// GammaCorrectionShader.js的功能就是进行伽马校正
// 具体点说就是可以用来解决gltf模型后处理时候,颜色偏差的问题
import {GammaCorrectionShader} from 'three/addons/shaders/GammaCorrectionShader.js';
// ShaderPass功能:使用后处理Shader创建后处理通道
import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js';
// 创建伽马校正通道
const gammaPass= new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaPass);
# 处理带来的锯齿
- FXAA抗锯齿通道,减弱了锯齿,但是并不完美
// ShaderPass功能:使用后处理Shader创建后处理通道
import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js';
// FXAA抗锯齿Shader
import { FXAAShader } from 'three/addons/shaders/FXAAShader.js';
// 设置设备像素比,避免canvas画布输出模糊
renderer.setPixelRatio(window.devicePixelRatio);
const FXAAPass = new ShaderPass( FXAAShader );
// `.getPixelRatio()`获取`renderer.setPixelRatio()`设置的值
const pixelRatio = renderer.getPixelRatio();// 获取设备像素比
// width、height是canva画布的宽高度
FXAAPass.uniforms.resolution.value.x = 1 /(width*pixelRatio);
FXAAPass.uniforms.resolution.value.y = 1 /(height*pixelRatio);
composer.addPass( FXAAPass );
- SMAA抗锯齿通道,相比FXAA抗锯齿效果更好一些
// SMAA抗锯齿通道
import {SMAAPass} from 'three/addons/postprocessing/SMAAPass.js';
// 获取.setPixelRatio()设置的设备像素比
const pixelRatio = renderer.getPixelRatio();
// width、height是canva画布的宽高度
const smaaPass = new SMAAPass(width * pixelRatio, height * pixelRatio);
composer.addPass(smaaPass);
# 射线拾取
# 射线Ray
// 创建射线对象Ray
const ray = new THREE.Ray()
// 设置射线起点
ray.origin = new THREE.Vector3(1,0,3);
ray.origin.set(1, 0, 3);
// 射线方向
ray.direction = new THREE.Vector3(1,0,0);// 表示射线沿着x轴正方向
ray.direction = new THREE.Vector3(-1,0,0);// 表示射线沿着x轴负方向
// 注意.direction的值需要是单位向量,不是的话可以执行.normalize()归一化或者说标准化
ray.direction = new THREE.Vector3(5,0,0).normalize();
intersectTriangle() 计算射线和三角形是否相交叉,相交返回交点,不相交返回null
// 三角形三个点坐标
const p1 = new THREE.Vector3(100, 25, 0);
const p2 = new THREE.Vector3(100, -25, 25);
const p3 = new THREE.Vector3(100, -25, -25);
const point = new THREE.Vector3();//用来记录射线和三角形的交叉点
// 参数4表示是否进行背面剔除
const result = ray.intersectTriangle(p1,p2,p3,false,point);
console.log('交叉点坐标', point);
console.log('查看是否相交', result);
注意
如果沿着三个点的顺序转圈是逆时针方向,表示正面,如果是顺时针方向,表示背面
如果参数4设为true,表示进行背面剔除,虽然从几何空间上讲,该射线和三角形交叉,但在threejs中,三角形背面对着射线,视为交叉无效,返回null
# 射线投射器 Raycaster
射线投射器 Raycaster 具有一个射线属性 ray
// origin:光线投射的原点向量
// direction:向射线提供方向的方向向量,应当被标准化
// near:近距离。near不能为负值,其默认值为0
// far:远距离。far不能小于near,其默认值为Infinity(正无穷)
Raycaster(origin:Vector3, direction:Vector3, near:Float, far:Float)
const raycaster = new THREE.Raycaster();
// 设置射线起点
raycaster.ray.origin = new THREE.Vector3(-100, 0, 0);
// 设置射线方向射线方向沿着x轴
raycaster.ray.direction = new THREE.Vector3(1, 0, 0);
# 检测与射线相交的网格模型
射线投射器 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);
if (intersects.length > 0) {
// 选中模型的第一个模型,设置为红色
intersects[0].object.material.color.set(0xff0000);
}
注意:射线拾取的时候,检测物体位置要确保更新的情况下,执行射线计算
model.updateMatrixWorld(true);
# 标准设备坐标系
Canvas画布有一个标准设备坐标系,该坐标系的坐标原点在画布的中间位置,x轴水平向右,y轴竖直向上
# 标准设备坐标系的坐标值不是绝对值,是相对值,范围是[-1,1]区间
# 也是说canvas画布上任何一个位置的坐标,如果用标准设备坐标系去衡量,那么坐标的所有值都在-1到1之间
# .offsetX 除以 canvas 画布宽度 width,就可以从绝对值变成相对值,范围是0~1,相对值乘以2,范围0~2
# 再减去1,范围是-1~1,刚好和canvas画布标准设备坐标的范围-1~1能够对应起来
# 鼠标点击选中模型
- 坐标转化(屏幕坐标转标准设备坐标)
// 监听鼠标事件
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;
- 计算射线 setFromCamera()
const raycaster = new THREE.Raycaster();
// .setFromCamera()计算射线投射器 Raycaster 的射线属性 ray
// 形象点说就是在点击位置创建一条射线,用来选中拾取模型对象
raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
- 射线交叉计算 intersectObjects()
// 第二个参数是否会检查所有的后代。否则将只会检查对象本身。默认值为true
const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);
if (intersects.length > 0) {
// 选中模型的第一个模型,设置为红色
intersects[0].object.material.color.set(0xff0000);
}
# canvas画布尺寸跟着浏览器窗口变化
// 每次触发click事件,都要重新计算canvas画布
renderer.domElement.addEventListener('click', function (event) {
const px = event.offsetX;
const py = event.offsetY;
// 屏幕坐标转WebGL标准设备坐标
const x = (px / window.innerWidth) * 2 - 1;
const y = -(py / window.innerHeight) * 2 + 1;
})
# 射线拾取Sprite控制场景
- 三维场景中提供了两个精灵模型对象,可以分别自定义一个方法.change()
sprite.change = function(){
mesh.material.color.set(0xffffff);
}
sprite2.change = function(){
mesh.material.color.set(0xffff00);
}
- 鼠标单击,如果选中某个精灵模型,就调用该精灵模型绑定的函数.change()
addEventListener('click', function (event) {
...
...
// 射线交叉计算拾取精灵模型
const intersects = raycaster.intersectObjects([sprite,sprite2]);
if (intersects.length > 0) {
intersects[0].object.change();// 执行选中sprite绑定的change函数
}
})
# 场景标注
# CSS2DRenderer
通过 CSS2DRenderer.js 可以把HTML元素作为标签标注到三维场景,特点
# 始终面向摄像机的平面
# 大小不随着相机远近而改变大小
CSS2DRenderer.js提供了两个类:
# 引入CSS2渲染器CSS2DRenderer和CSS2模型对象CSS2DObject
import { CSS2DObject,CSS2DRenderer } from 'three/addons/renderers/CSS2DRenderer.js';
CSS2渲染器(CSS2DRenderer)
# 和常用的WebGL渲染器 WebGLRenderer 一样都是渲染器,只是渲染模型对象不同
# 它用来渲染HTML元素标签对应的 CSS2模型对象 CSS2DObject
CSS2模型对象(CSS2DObject)
# 可以把一个HTML元素转化为一个类似threejs网格模型的对象
# 像网格模型一样去设置位置.position或添加到场景中
# 创建模型对象 CSS2DObject
import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
const div = document.getElementById('tag');
// HTML元素转化为threejs的CSS2模型对象
const tag = new CSS2DObject(div);
tag.position.set(50,0,50);
scene.add(tag);
注意 vue3项目中
const myDiv = ref(null);
// 在件挂载完成后
onMounted(() => {
// 创建模型对象
// 渲染HTML标签
})
# 渲染器 CSS2DRenderer
import { CSS2DRenderer } from 'three/addons/renderers/CSS2DRenderer.js';
// 创建一个CSS2渲染器CSS2DRenderer
const css2Renderer = new CSS2DRenderer();
// width, height:canvas画布宽高度
css2Renderer.setSize(width, height);
css2Renderer.domElement.style.position = "absolute";
css2Renderer.domElement.style.zIndex = "9999999";
css2Renderer.domElement.style.top = "0px";
// 可以解决HTML标签遮挡Canvas画布事件
css2Renderer.domElement.style.pointerEvents = "none";
// 你可以插入到web网页中任何你想放的位置
document.body.appendChild(css2Renderer.domElement);
// 用法和webgl渲染器渲染方法类似
function render() {
//renderer.render(scene, camera);
css2Renderer.render(scene, camera);
requestAnimationFrame(render);
}
# 调整HTML标签遮挡Canvas画布事件
css2Renderer.domElement.style.pointerEvents = 'none';
// 如果场景中某个元素需要鼠标事件,可以单独设置
<img id="close" src="./关闭.png" style="pointer-events: auto;"/>
// 或者
document.getElementById('close').style.pointerEvents = 'auto';
# 调整HTML元素标签和canvas画布的前后顺序
renderer.domElement.style.zIndex = 1;
css2Renderer.domElement.style.zIndex = -1;
# 调整HTML元素的位置
const div = document.getElementById('tag');
div.style.top = '-161px'; //平移-161px,指示线端点和标注点重合
// div.style.top = '-140px'; //可以在-161px基础上微调
// div.style.left = 'px';
# 调整HTML标签渲染前隐藏
<div id="tag" style="display: none;">
# Canvas全屏尺寸变化,CSS2渲染器设置
window.onresize = function () {
const width = window.innerWidth;
const height = window.innerHeight;
// cnavas画布宽高度重新设置
renderer.setSize(width,height);
// HTML标签css2Renderer.domElement尺寸重新设置
css2Renderer.setSize(width,height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
};
# CSS3DRenderer
- CSS3渲染器
CSS3DRenderer
和CSS2渲染器CSS2DRenderer
整体使用流程基本相同 - CSS3渲染的标签会跟着场景相机同步缩放,而CSS2渲染的标签默认保持自身像素值
# 创建模型对象 CSS3DObject
import { CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';
const div = document.getElementById('tag');
const tag = new CSS3DObject(div);
tag.position.y += 80;
mesh.add(tag);
# 渲染器 CSS3DRenderer
- CSS3模型对象
CSS3DObject
渲染结果,就像一个矩形平面网格模型一样 - CSS2模型对象和CSS3模型对象,旋转HTML元素标签的正反面都可以看到
<div id="tag" style="backface-visibility: hidden;">标签内容</div>
# 精灵模型 CSS3DSprite
- CSS3精灵模型
CSS3DSprite
对应的 HTML 标签,可以跟着场景缩放,位置可以跟着场景旋转 - 自身的姿态角度始终平行于 canvas 画布,不受旋转影响,就像精灵模型一样 Sprite
import { CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js';
const div = document.getElementById('tag');
// HTML元素转化为threejs的CSS3精灵模型`CSS3DSprite`
const tag = new CSS3DSprite(div);
//标签tag作为mesh子对象,默认标注在模型局部坐标系坐标原点
mesh.add(tag);
// 相对父对象局部坐标原点偏移80,刚好标注在圆锥
tag.position.y += 80;
CSS3DSprite和精灵模型的区别
精灵模型渲染的 Sprite 标签,默认可以被其他网格模型遮挡
但是CSS3渲染的标签是叠加在canvas画布上,不会被其它网格模型遮挡
# TweenJS动画
TweenJS是一个由JavaScript语言编写的补间动画库,对于任何前端项目,都可以选择tweenjs库
// npm安装
npm i @tweenjs/tween.js@^18
import TWEEN from '@tweenjs/tween.js';
# 基本语法
tweenjs功能从语法的角度讲,就是改变自己的参数对象
// 创建一段mesh平移的动画
const tween = new TWEEN.Tween(mesh.position);
// 经过2000毫秒,mesh对象的x和y属性分别从零变化为100、50
tween.to({x: 100,y: 50}, 2000);
// tween动画开始执行
tween.start();
// 简洁写法
const tween = new TWEEN.Tween(mesh.position).to({x: 100,y: 50}, 2000).start();
const tween = new TWEEN.Tween(mesh.scale).to({x: 100,y: 50}, 2000).start();// 模型缩放
// 在requestAnimationFrame动画中,更新tween
function loop() {
TWEEN.update();
requestAnimationFrame(loop);
}
# 回调函数
onStart # 动画开始执行触发
onUpdate # 动画执行过程中,一直被调用执行
onComplete # 动画正常执行完触发
# 循环方式
// 循环无数次
tween.repeat(Infinity);
// 循环2次
tween.repeat(2);
// 循环往复
tween.yoyo(true);
// 延迟3秒
tween.delay(3080);
// 切换到另一个动画tween2
tween.chain(tween2);
# 缓动算法.easing
动画片段 tween 通过 .easing() 方法可以设置缓动算法
camera.position.set(3000, 3000, 3000);
camera.lookAt(0, 0, 0);
// 视觉效果:地球从小到大出现(透视投影相机远小近大投影规律)
new TWEEN.Tween(camera.position)
.to({x: 300,y: 300,z: 300}, 3000)
.start()
.easing(TWEEN.Easing.Quadratic.Out);//使用二次缓动函数
easing类型有多种,在tween.js-master\examples\03_graphs.html 可以查看各种缓动算法的曲线效果图
// 动画开始缓动方式(类比加速启动)
tween.easing(TWEEN.Easing.Sinusoidal.In);
// 动画结束缓动方式(类比减速刹车)
tween.easing(TWEEN.Easing.Sinusoidal.Out);
// 同时设置In和Out
tween.easing(TWEEN.Easing.Sinusoidal.InOut);
# 相机运动动画
使用tweenjs改变相机的位置camera.position和视线方向
camera.position.set(202, 123, 125);
const tween = new TWEEN.Tween(camera.position).to({x: 202,y: 123,z: 50}, 3000).start()
相机运动过程中需要重新计算相机视线方向,可以在相机位置改变的过程中不停地执行lookAt()即可
camera.position.set(202, 123, 125);
camera.lookAt(0, 0, 0);
new TWEEN.Tween(camera.position)
.to({x: 202,y: 123,z: -350}, 3000)
// tweenjs改变参数对象的过程中,.onUpdate方法会被重复调用执行
.onUpdate(function(){
camera.lookAt(0, 0, 0);
})
.start()
# 相机圆周运动
const R = 100; //相机圆周运动的半径
new TWEEN.Tween({angle:0})
.to({angle: Math.PI*2}, 16000)
.onUpdate(function(obj){
camera.position.x = R * Math.cos(obj.angle);
camera.position.z = R * Math.sin(obj.angle);
// 保持相机镜头对准坐标原点
camera.lookAt(0, 0, 0);
})
.start()
# 相机飞行靠近观察设备
相机从当前位置 camera.position 飞行三维场景中某个世界坐标附近,和观察目标拉开一定的距离
const pos = new THREE.Vector3();
mesh2.getWorldPosition(pos); //获取三维场景中某个对象世界坐标
// 相机飞行到的位置和观察目标拉开一定的距离
const pos2 = pos.clone().addScalar(30);//向量的x、y、z坐标分别在pos基础上增加30
// 相机从当前位置camera.position飞行三维场景中某个世界坐标附近
new TWEEN.Tween({
// 相机开始坐标
x: camera.position.x,
y: camera.position.y,
z: camera.position.z,
// 相机开始指向的目标观察点
tx: 0,
ty: 0,
tz: 0,
})
.to({
// 相机结束坐标
x: pos2.x,
y: pos2.y,
z: pos2.z,
// 相机结束指向的目标观察点
tx: pos.x,
ty: pos.y,
tz: pos.z,
}, 2000)
.onUpdate(function (obj) {
// 动态改变相机位置
camera.position.set(obj.x, obj.y, obj.z);
// 动态计算相机视线
camera.lookAt(obj.tx, obj.ty, obj.tz);
})
.start()
.onComplete(function(obj){
// 相机指向的目标改变以后,该相机控件也需要更新
controls.target.set(obj.tx, obj.ty, obj.tz);
controls.update();
})
# 关键帧动画
所谓关键帧动画,可以理解为在时间轴上,选择几个关键的时间点,然后分别定义这几个时间点对应物体状态(比如位置、姿态、颜色等),然后基于几个关键的时间 —— 状态数据,生成连续的动画。
# 几何体状态动画
- 创建关键帧动画 AnimationClip
// 1、给需要设置关键帧动画的模型命名
mesh.name = "Box";
// 2、设置关键帧数据KeyframeTrack
const times = [0, 3, 6]; //时间轴上,设置三个时刻0、3、6秒
// times中三个不同时间点,物体分别对应values中的三个xyz坐标
const values = [0, 0, 0, 100, 0, 0, 0, 0, 100];
// 0~3秒,物体从(0,0,0)逐渐移动到(100,0,0),3~6秒逐渐从(100,0,0)移动到(0,0,100)
const posKF = new THREE.KeyframeTrack('Box.position', times, values);
// 从2秒到5秒,物体从红色逐渐变化为蓝色
const colorKF = new THREE.KeyframeTrack('Box.material.color', [2, 5], [1, 0, 0, 0, 0, 1]);
// 3、 基于关键帧数据,创建关键帧动画对象,命名"test",持续时间6秒
const clip = new THREE.AnimationClip("test", 6, [posKF, colorKF]);
- 播放关键帧动画 AnimationClip
// 包含关键帧动画的模型对象作为 AnimationMixer 的参数创建一个播放器 mixer
const mixer = new THREE.AnimationMixer(mesh);
// 执行播放器AnimationMixer的.clipAction()方法返回一个AnimationAction对象
const clipAction = mixer.clipAction(clip);
//.play()控制动画播放,默认循环播放
clipAction.play();
- 更新播放器 AnimationMixer
const clock = new THREE.Clock();
function loop() {
requestAnimationFrame(loop);
const frameT = clock.getDelta();
// 更新播放器相关的时间
mixer.update(frameT);
}
loop();
# 动画动作对象 AnimationAction
就是用来控制如何播放关键帧动画,比如是否播放、几倍速播放、是否循环播放、是否暂停播放
// 执行播放器AnimationMixer的.clipAction()方法会返回一个AnimationAction对象
const clipAction = mixer.clipAction(clip);
// .play()控制动画播放,默认循环播放
clipAction.play();
// 不循环播放
clipAction.loop = THREE.LoopOnce;
// 物体状态停留在动画结束的时候
clipAction.clampWhenFinished = true;
// 动画暂停状态
clipAction.paused = true;
// 动画停止结束,回到开始状态
clipAction.stop();
// 倍速播放
clipAction.timeScale = 1;//默认
clipAction.timeScale = 2;//2倍速
// 拖动条调整播放速度
const gui = new GUI(); //创建GUI对象
// 0~6倍速之间调节
gui.add(clipAction, 'timeScale', 0, 6);
// 在暂停情况下,设置.time属性,把动画定位在任意时刻
clipAction.paused = true;
clipAction.time = 1; // 物体状态为动画1秒对应状态
clipAction.time = 3; // 物体状态为动画3秒对应状态
# 动画任意时间状态 time
// 参数2:如果为单个数字,表示持续的时间s
const clip = new THREE.AnimationClip("test", 6, [posKF, colorKF]);
//动画动作对象 AnimationAction 设置开始播放时间:从1秒时刻对应动画开始播放
clipAction.time = 1;
//关键帧动画对象 AnimationClip 设置播放结束时间:到5秒时刻对应的动画状态停止
clip.duration = 5;
# 权重属性 weight
动画动作对象AnimationAction的权重属性.weight可以控制动画的执行,权重为0,对应动画不影响人的动作,权重为1影响程度最大。
const IdleAction = mixer.clipAction(gltf.animations[0]);
const RunAction = mixer.clipAction(gltf.animations[1]);
const WalkAction = mixer.clipAction(gltf.animations[3]);
IdleAction.play();
RunAction.play();
WalkAction.play();
// 跑步和走路动画对人影响程度为0,人处于休闲状态
IdleAction.weight = 1.0;
RunAction.weight = 0.0;
WalkAction.weight = 0.0;
# 解析外部模型关键帧动画
比如Blender,生成关键帧动画,导出包含动画的模型文件gltf、fbx等,加载模型后,你只需要播放关键帧动画,而不用手写代码创建关键帧动画。
const loader = new GLTFLoader();
loader.load("../工厂.glb", function (gltf) {
console.log('控制台查看gltf对象结构', gltf);
// 获取帧动画数据 gltf.animations
// gltf.animations是一个数组,存储多个Clip动画对象 AnimationClip
console.log('动画数据', gltf.animations);
})
播放AnimationClip动画
loader.load("Soldier.glb", function (gltf) {
model.add(gltf.scene);
//包含关键帧动画的模型作为参数创建一个播放器
const mixer = new THREE.AnimationMixer(gltf.scene);
// 获取gltf.animations[0]的第一个clip动画对象
const clipAction = mixer.clipAction(gltf.animations[0]); //创建动画clipAction对象
clipAction.play(); //播放动画
// 如果想播放动画,需要周期性执行`mixer.update()`更新AnimationMixer时间数据
const clock = new THREE.Clock();
function loop() {
requestAnimationFrame(loop);
//clock.getDelta()方法获得loop()两次执行时间间隔
const frameT = clock.getDelta();
// 更新播放器相关的时间
mixer.update(frameT);
}
loop();
})
# 几何体变形动画
- 几何体两组顶点一一对应,位置不同,然后通过权重系数,可以控制模型形状在两组顶点之间变化
- BufferGeometry 的属性 morphAttributes 的功能就是用来设置几何体变形目标顶点数据
const geometry = new THREE.BoxGeometry(50, 50, 50);
// 为geometry提供变形目标的顶点数据(注意和原始geometry顶点数量一致)
const target1 = new THREE.BoxGeometry(50, 200, 50).attributes.position;//变高
const target2 = new THREE.BoxGeometry(10, 50, 10).attributes.position;//变细
// 几何体顶点变形目标数据,可以设置1组或多组
geometry.morphAttributes.position = [target1, target2];
const mesh = new THREE.Mesh(geometry, material);
注意
给一个几何体 geometry 设置顶点变形数据 morphAttributes 时候要注意,在执行代码new THREE.Mesh()之前设置,否则报错
# 权重系数控制变形程度 morphTargetInfluences
网格模型Mesh、点模型、线模型都有一个权重属性morphTargetInfluences
,该权重的作用是,控制geometry自身顶点和变形目标顶点分别对模型形状形象程度,范围一般0~1
// mesh在geometry原始形状和变形目标1顶点对应形状之间变化
//权重0:物体形状对应geometry.attributes.position的形状
mesh.morphTargetInfluences[0] = 0.0;
//权重0.5:物体形状对应geometry和target1变形中间的状态
mesh.morphTargetInfluences[0] = 0.5;
//权重1:物体形状对应target1的形状
mesh.morphTargetInfluences[0] = 1.0;
// mesh在geometry原始形状和变形目标2顶点对应形状之间变化
mesh.morphTargetInfluences[1] = 0.5;
生成变形动画的方法非常简单,你只是需要通过关键帧动画,改变模型的变形权重系数即可
// 创建变形动画权重系数的关键帧数据
mesh.name = "Box";//关键帧动画控制的模型对象命名
// 设置变形目标1对应权重随着时间的变化:0~5秒,物体变高
const KF1 = new THREE.KeyframeTrack('Box.morphTargetInfluences[0]', [0, 5], [0, 1]);
// 设置变形目标2对应权重随着时间的变化:5~10秒,物体变细
const KF2 = new THREE.KeyframeTrack('Box.morphTargetInfluences[1]', [5, 10], [0, 1]);
// 创建一个剪辑clip对象
const clip = new THREE.AnimationClip("t", 10, [KF1, KF2]);
# 解析外部模型变形动画
loader.load("../人.glb", function (gltf) {
console.log('控制台查看gltf对象结构', gltf);
model.add(gltf.scene);
// 访问人体网格模型
const mesh = gltf.scene.children[0];
// 获取所有变形目标的顶点数据
const tArr = mesh.geometry.morphAttributes.position
console.log('所有变形目标', tArr);
console.log('所有权重', mesh.morphTargetInfluences);
})
外部模型变形数据生成动画
loader.load("../人.glb", function (gltf) {
const mesh = gltf.scene.children[0];
// 创建变形动画权重系数的关键帧数据
mesh.name = "per"; //关键帧动画控制的模型对象命名
// 设置变形目标1对应权重随着时间的变化
const KF1 = new THREE.KeyframeTrack('per.morphTargetInfluences[0]', [0, 5], [0, 1]);
// 生成关键帧动画
const clip = new THREE.AnimationClip("t", 5, [KF1]);
//包含关键帧动画的模型作为参数创建一个播放器
const mixer = new THREE.AnimationMixer(gltf.scene);
const clipAction = mixer.clipAction(clip);
clipAction.play();
const clock = new THREE.Clock();
function loop() {
requestAnimationFrame(loop);
const frameT = clock.getDelta();
// 更新播放器相关的时间
mixer.update(frameT);
}
loop();
})
# 骨骼动画
骨骼关节Bone的父类是Object3D,自然会继承Object3D相关的属性或方法,比如位置属性position、旋转方法rotateX()、添加方法add()
# 骨骼关节 Bone
人或动物实际的骨骼关节结构往往是比较复杂的,一般可以用一个层级树结构表达
const Bone1 = new THREE.Bone(); //关节1,用来作为根关节
const Bone2 = new THREE.Bone(); //关节2
const Bone3 = new THREE.Bone(); //关节3
// 设置关节父子关系 多个骨头关节构成一个树结构
Bone1.add(Bone2);
Bone2.add(Bone3);
# 可视化骨骼关节 SkeletonHelper
// 骨骼关节可以和普通网格模型一样作为其他模型子对象,添加到场景中
const group = new THREE.Group();
group.add(Bone1);
// SkeletonHelper会可视化参数模型对象所包含的所有骨骼关节
const skeletonHelper = new THREE.SkeletonHelper(group);
group.add(skeletonHelper);
# 蒙皮网格 SkinnedMesh
SkinnedMesh 的父类就是网格模型 Mesh,都是网格模型,用来表达一个物体的外表面
// 如果存在蒙皮网格模型的话,可以通过加载返回gltf对象的场景属性scene查看
const loader = new GLTFLoader();
loader.load("../骨骼动画.glb", function (gltf) {
console.log('控制台查看gltf对象结构', gltf);
model.add(gltf.scene);
// 根据节点名字获取某个骨骼网格模型
const SkinnedMesh = gltf.scene.getObjectByName('身体');
console.log('骨骼网格模型', SkinnedMesh);
})
# 蒙皮网格模型的骨架 SkinnedMesh.skeleton
SkinnedMesh相比较Mesh区别就是,可以跟着自己的骨架.skeleton变化,比如骨架里面的骨骼关节Bone旋转,会带动附近骨骼网格模型SkinnedMesh跟着旋转
// 根据节点名字获取某个骨骼网格模型
const SkinnedMesh = gltf.scene.getObjectByName('身体');
console.log('骨架', SkinnedMesh.skeleton);
# 骨架的骨骼关节属性 skeleton.bones
骨架SkinnedMesh.skeleton的关节属性.bones是一个数组包含了所有骨骼关节
console.log('骨架所有关节', SkinnedMesh.skeleton.bones);
console.log('根关节', SkinnedMesh.skeleton.bones[0]);
# 八叉树碰撞检测
为了实现漫游的碰撞检测功能,比如遇到装障碍物被挡住、比如爬坡和上楼梯
# 八叉树基本原理解释
# 大家都知道网格模型Mesh本质是由三角形构成,三角形由顶点构成,如果整个3D模型用一个长方体空间来表示
# 在三维空间xyz三个方向,都分割一次,这样就可以得到8个小的长方体子空间
# 简单使用
// 引入八叉树扩展库
import { Octree } from 'three/examples/jsm/math/Octree.js';
// 实例化一个八叉树对象
const worldOctree = new Octree();
const gltf = await loader.loadAsync("../地形.glb");
// .fromGraphNode()的参数是模型对象,比如一个mesh,或者多个mesh构成的层级模型
// 会把3D模型分割为8个子空间,每个子空间都包含对应的三角形或者顶点数据,每个子空间还可以继续分割
worldOctree.fromGraphNode(gltf.scene);
# 浏览器控制台打印八叉树
console.log('查看八叉树结构', worldOctree);
// .box属性是包围盒Box3,描述当前分割的子空间位置和尺寸
// .subTrees属性表示八叉树的子节点,类似threejs层级模型的children属性
// 查看叶子结点(最后一层没有子对象的节点).triangles属性,可以看到包含的三角形数据
# 可视化八叉树
import { OctreeHelper } from 'three/examples/jsm/helpers/OctreeHelper.js';
const helper = new OctreeHelper(worldOctree);
scene.add(helper);
# 引入胶囊碰撞体 Capsule.js
// 引入/examples/jsm/math/目录下胶囊扩展库Capsule.js
import { Capsule } from 'three/examples/jsm/math/Capsule.js';
// 创建胶囊几何体
const R = 0.4;// 胶囊半径
const H = 1.7;// 胶囊总高度
const start = new THREE.Vector3(0, R, 0);// 底部半球球心坐标
const end = new THREE.Vector3(0, H - R, 0);// 顶部半球球心坐标
const capsule = new Capsule(start, end, R);
// 可视化胶囊几何体
const capsuleHelper = CapsuleHelper(R, H);
model.add(capsuleHelper);
# 交叉计算
Octree.capsuleIntersect(capsule)
可以计算Octree表示的3D模型与胶囊几何体capsule是否重合交叉,如果有重合交叉,返回交叉相关的信息,具体说就是在某个方向上交叉重合的深度是多少
const result = worldOctree.capsuleIntersect(capsule);
console.log('碰撞检测结果', result);
// .depth交叉重合的深度
// .normal深度对应的方向
# 根据交叉碰撞数据,平移碰撞体
.normal数据的特点就是让胶囊沿着.normal方向,平移.depth距离,就能刚好确保交叉重合深度为0
// 根据碰撞结果平移胶囊碰撞体,使交叉重合深度为0
capsule.translate(result.normal.multiplyScalar(result.depth));
capsuleHelper.position.copy(capsule.start);
capsuleHelper.position.y -= R;
实用案例 →