内容参考自Web3D可视化系统课程
// 30:视场角度, width / height:Canvas画布宽高比, 1:近裁截面, 3000:远裁截面
const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(800, 500); //设置three.js渲染区域的尺寸(像素px)
renderer.render(scene, camera); //执行渲染操作
document.getElementById('webgl').appendChild(renderer.domElement); //将画布绑定到页面元素中
//环境光:没有特定方向,整体改变场景的光照明暗
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient);
// 平行光:有光源位置方向和光源指向方向
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
// 设置光源的方向:通过光源position属性和目标指向对象的position属性计算
directionalLight.position.set(80, 100, 50);
// 方向光指向对象网格模型mesh,可以不设置,默认的位置是0,0,0
directionalLight.target = mesh;
scene.add(directionalLight);
// requestAnimationFrame实现周期性循环执行
// requestAnimationFrame默认每秒钟执行60次,但不一定能做到,要看代码的性能
let i = 0;
function render() {
i+=1;
console.log('执行次数'+i);
requestAnimationFrame(render);//请求再次执行函数render
}
render();
const renderer = new THREE.WebGLRenderer({
// 渲染器开启抗锯齿
antialias:true,
});
// 设置你屏幕对应的设备像素比,以免渲染模糊问题
renderer.setPixelRatio(window.devicePixelRatio);
//设置背景颜色
renderer.setClearColor(0x444444, 1);
dat.gui.js就是一个前端js库,对HTML、CSS和JavaScript进行了封装,学习开发的时候,借助dat.gui.js可以快速创建控制三维场景的UI交互界面
// 引入dat.gui.js的一个类GUI
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
// 实例化一个gui对象
const gui = new GUI();
//改变交互界面style属性
gui.domElement.style.right = '0px';
gui.domElement.style.width = '300px';
//创建一个对象,对象属性的值可以被GUI库创建的交互界面改变
const obj = {
x: 30,
};
// gui增加交互界面,用来改变obj对应属性
gui.add(obj, 'x', 0, 100);
// .name()方法 改变gui生成交互界面显示的内容
gui.add(ambient, '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;
});
// .addColor()生成颜色值改变的交互界面
const obj = {
color:0x00ffff,
};
// .addColor()生成颜色值改变的交互界面
gui.addColor(obj, 'color').onChange(function(value){
mesh.material.color.set(value);
});
// 如果参数3、参数4数据类型为数字,则gui生成拖动条
gui.add(obj, 'x', 0, 180).onChange(function (value) {
mesh.position.x = value;
});
// 如果参数3数据类型为数组,则gui生成下拉菜单
const obj = {
scale: 0,
};
gui.add(obj, 'scale', [-100, 0, 100]).name('y坐标').onChange(function (value) {
mesh.position.y = value;
});
// 如果参数3数据类型为对象,gui也生成下拉菜单
const obj = {
scale: 0,
};
gui.add(obj, 'scale', {
left: -100,
center: 0,
right: 100
}).name('位置选择').onChange(function (value) {
mesh.position.x = value;
});
// 如果参数为布尔类型,则gui生成单选框
const obj = {
bool: false,
};
gui.add(obj, 'bool').name('是否旋转');
// 当gui交互界面要控制的属性过多的情况下,为了避免混合,需要用到分组管理
// 通过gui对象的.addFolder()方法可以创建一个子菜单
// .close()关闭菜单和.open()展开菜单
const gui = new GUI(); //创建GUI对象
const obj = {
color: 0x00ffff,// 材质颜色
};
// 创建材质子菜单
const matFolder = gui.addFolder('材质');
matFolder.close();
// 材质颜色color
matFolder.addColor(obj, 'color').onChange(function(value){
material.color.set(value);
});
// 材质高光颜色specular
matFolder.addColor(obj, 'specular').onChange(function(value){
material.specular.set(value);
});
// 创建环境光子菜单
const ambientFolder = gui.addFolder('环境光');
// 环境光强度
ambientFolder.add(ambient, 'intensity',0,2);
// 平行光强度
dirFolder.add(directionalLight, 'intensity',0,2);
// 平行光位置
dirFolder.add(directionalLight.position, 'x',-400,400);
dirFolder.add(directionalLight.position, 'y',-400,400);
dirFolder.add(directionalLight.position, 'z',-400,400);
// BufferGeometry是一个没有任何形状的空几何体,你可以通过BufferGeometry自定义任何几何形状,具体一点说就是定义顶点数据
// 创建一个空的几何体对象
const geometry = new THREE.BufferGeometry();
//类型化数组创建顶点数据
const vertices = new Float32Array([
0, 0, 0, //顶点1坐标
50, 0, 0, //顶点2坐标
0, 100, 0, //顶点3坐标
0, 0, 10, //顶点4坐标
0, 0, 100, //顶点5坐标
50, 0, 10, //顶点6坐标
]);
// 创建属性缓冲区对象 3个为一组,表示一个顶点的xyz坐标
const attribue = new THREE.BufferAttribute(vertices, 3);
// 设置几何体attributes属性的位置属性
geometry.attributes.position = attribue;
// 点渲染模式
const material = new THREE.PointsMaterial({
color: 0xffff00,
size: 10.0 //点对象像素尺寸
});
// 几何体geometry作为点模型Points参数,会把几何体渲染为点
const points = new THREE.Points(geometry, material); //点模型对象
网格模型mesh原理是由n个三角形拼接构成,使用使用网格模型Mesh渲染几何体geometry,就是几何体所有顶点坐标三个为一组,构成一个三角形,多组顶点构成多个三角形,就可以用来模拟表示物体的表面。
网格模型有着正反面之分,默认的情况的下一个三角形只会显示它的正面,threejs的正反面是根据三角形坐标点渲染的顺序来区分的,如果三个点的顺序为顺时针,则为正面,反之则为背面
也可以通过设置材质的属性来规定那一面可见
const material = new THREE.MeshBasicMaterial({
color: 0x0000ff, //材质颜色
side: THREE.FrontSide, //只有正面可见
});
const material = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide, //两面可见
});
const material = new THREE.MeshBasicMaterial({
side: THREE.BackSide, //设置只有背面可见
});
网格模型Mesh对应的几何体BufferGeometry,拆分为多个三角后,很多三角形重合的顶点位置坐标是相同的,这时候如果你想减少顶点坐标数据量,可以借助几何体顶点索引geometry.index
来实现。
每个三角形3个顶点坐标,矩形平面可以拆分为两个三角形,也就是6个顶点坐标。
如果几何体有顶点索引geometry.index
,那么你可以吧三角形重复的顶点位置坐标删除。
const vertices = new Float32Array([
0, 0, 0, //顶点1坐标
80, 0, 0, //顶点2坐标
80, 80, 0, //顶点3坐标
0, 80, 0, //顶点4坐标
]);
通过javascript类型化数组Uint16Array
创建顶点索引.index
数据。
// Uint16Array类型数组创建顶点索引数据
const indexes = new Uint16Array([
// 下面索引值对应顶点位置数据中的顶点坐标
0, 1, 2, 0, 2, 3,
])
通过threejs的属性缓冲区对象BufferAttribute表示几何体顶点索引.index
数据。
// 索引数据赋值给几何体的index属性
geometry.index = new THREE.BufferAttribute(indexes, 1); //1个为一组
当材质由MeshBasicMaterial材质改为MeshLambertMaterial这种受光照影响的材质,会发现原来的矩形平面无法渲染。原因就是使用了受光照影响的材质,几何体BufferGeometry需要定义顶点法线数据。
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);
BufferGeometry通过.scale()
、.translate()
、.rotateX()
、.rotateY()
等方法可以对几何体本身进行缩放、平移、旋转,这些方法本质上都是改变几何体的顶点数据。
// 几何体xyz三个方向都放大2倍
geometry.scale(2, 2, 2);
// 几何体沿着x轴平移50
geometry.translate(50, 0, 0);
// 几何体绕着x轴旋转45度
geometry.rotateX(Math.PI / 4);
模型的角度属性rotation和quaternion都是表示模型角度的状态,只是表达方法不同,rotation属性的值是欧拉对象Euler,而quaternion属性的值是四元数对象Quaternion
由于刚入门,就先介绍比较容易理解的角度属性rotation
和对应属性值欧拉对象Euler
// 创建一个欧拉对象,表示绕着xyz轴分别旋转45度,0度,90度
const Euler = new THREE.Euler( Math.PI/4,0, Math.PI/2);
//绕y轴的角度设置为60度
mesh.rotation.y += Math.PI/3;
//绕y轴的角度增加60度
mesh.rotation.y += Math.PI/3;
//绕y轴的角度减去60度
mesh.rotation.y -= Math.PI/3;
克隆.clone()
、复制.copy()
是threejs很多对象都具有的方法,比如三维向量对象Vector3、网格模型Mesh、几何体、材质。
// 克隆
const v1 = new THREE.Vector3(1, 2, 3);
console.log('v1',v1);
//v2是一个新的Vector3对象,和v1的.x、.y、.z属性值一样
const v2 = v1.clone();
console.log('v2',v2);
// 复制
const v1 = new THREE.Vector3(1, 2, 3);
const v3 = new THREE.Vector3(4, 5, 6);
//读取v1.x、v1.y、v1.z的赋值给v3.x、v3.y、v3.z
v3.copy(v1);
.clone()方法会创建一个与原始对象完全相同的新对象,包括其属性和方法。.copy()方法也会创建一个新对象,但它只复制原始对象的属性,而不包括其方法。
因此,.clone()方法更适合创建独立的对象实例,而.copy()方法更适合创建基于现有对象的变体。
递归遍历traverse()方法
Threejs层级模型就是一个树结构,可以通过递归遍历的算法去遍历Threejs一个模型对象包含的所有后代。
// 递归遍历model包含所有的模型节点
model.traverse(function(obj) {
console.log('所有模型节点的名称',obj.name);
// obj.isMesh:if判断模型对象obj是不是网格模型'Mesh'
if (obj.isMesh) {//判断条件也可以是obj.type === 'Mesh'
obj.material.color.set(0xffff00);
}
});
查找某个具体的模型getObjectByName()方法
// 返回名.name为"4号楼"对应的对象
const nameNode = scene.getObjectByName ("4号楼");
nameNode.material.color.set(0xff0000);
有时候模型可能有多个层级,那么模型的坐标就分为本地坐标和世界坐标了。
本地坐标就是模型自身的position属性,而世界坐标就是模型自身的position属性再加上所有父模型的position属性
getWorldPosition()
获取世界坐标// 声明一个三维向量用来表示某个坐标
const worldPosition = new THREE.Vector3();
// 获取mesh的世界坐标,你会发现mesh的世界坐标受到父对象group的.position影响
mesh.getWorldPosition(worldPosition);
console.log('世界坐标',worldPosition);
console.log('本地坐标',mesh.position);
模型对象的父类Object3D
封装了一个属性visible
,通过该属性可以隐藏或显示一个模型。
mesh.visible =false;// 隐藏一个网格模型,visible的默认值是true
group.visible =false;// 隐藏一个包含多个模型的组对象group
通过纹理贴图加载器TextureLoader
的load()
方法加载一张图片可以返回一个纹理对象Texture
,纹理对象Texture
可以作为模型材质颜色贴图.map
属性的值。
const geometry = new THREE.PlaneGeometry(200, 100);
//纹理贴图加载器TextureLoader
const texLoader = new THREE.TextureLoader();
// .load()方法加载图像,返回一个纹理对象Texture
const texture = texLoader.load('./earth.jpg');
const material = new THREE.MeshLambertMaterial({
// 设置纹理贴图:Texture对象作为材质map属性的属性值
map: texture,//map表示材质的颜色贴图属性
// 通常设置了纹理贴图后就不需要设置材质的颜色,这两者同时使用可能会引起颜色冲突
// color: 0x00ffff,
});
顶点UV坐标的作用是从纹理贴图上提取像素映射到网格模型Mesh的几何体表面上。
顶点UV坐标geometry.attributes.uv
和顶点位置坐标geometry.attributes.position
是一一对应的,
UV顶点坐标你可以根据需要在0~1之间任意设置,具体怎么设置,要看你想把图片的哪部分映射到Mesh的几何体表面上。
/**纹理坐标0~1之间随意定义*/
const uvs = new Float32Array([
0, 0, //图片左下角
1, 0, //图片右下角
1, 1, //图片右上角
0, 1, //图片左上角
]);
// 设置几何体attributes属性的位置normal属性
geometry.attributes.uv = new THREE.BufferAttribute(uvs, 2); //2个为一组,表示一个顶点的纹理坐标
const geometry = new THREE.PlaneGeometry(2000, 2000);
//纹理贴图加载器TextureLoader
const texLoader = new THREE.TextureLoader();
// .load()方法加载图像,返回一个纹理对象Texture
const texture = texLoader.load('./瓷砖.jpg');
// 设置阵列
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
// uv两个方向纹理重复数量
texture.repeat.set(30,30);//注意选择合适的阵列数量
const material = new THREE.MeshLambertMaterial({
// color: 0x00ffff,
// 设置纹理贴图:Texture对象作为材质map属性的属性值
map: texture,//map表示材质的颜色贴图属性
});
const mesh = new THREE.Mesh(geometry, material);
// 旋转矩形平面
mesh.rotateX(-Math.PI/2);
three.js项目开发中,把一个背景透明的.png
图像作为平面矩形网格模型Mesh的颜色贴图是一个非常有用的功能,通过这样一个功能,可以对three.js三维场景进行标注。
// 矩形平面网格模型设置背景透明的png贴图
const geometry = new THREE.PlaneGeometry(60, 60); //默认在XOY平面上
const textureLoader = new THREE.TextureLoader();
const material = new THREE.MeshBasicMaterial({
map: textureLoader.load('./指南针.png'),
transparent: true, //使用背景透明的png贴图,注意开启透明计算
});
const mesh = new THREE.Mesh(geometry, material);
// 引入GLTFLoader.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
// 创建GLTF加载器
const loader = new GLTFLoader();
// 加载.gltf文件
loader.load( 'gltf模型.gltf', function ( gltf ) {
scene.add( gltf.scene );
})
gltf.scene.traverse(function(obj) {
if (obj.isMesh) {//判断是否是网格模型
console.log('模型节点',obj);
console.log('模型节点名字',obj.name);
}
});
通过.name
标记材质,测试mesh1和mesh2是否共享了材质
const mesh1 = gltf.scene.getObjectByName("1号楼");
mesh1.material.name = '楼房材质';//通过name标记mesh1对应材质
const mesh2 = gltf.scene.getObjectByName("2号楼");
//通过name相同,可以判断mesh1.material和mesh2.material共享了同一个材质对象
console.log('mesh2.material.name', mesh2.material.name);
PBR材质简介
所谓PBR就是,基于物理的渲染(physically-based rendering)。
Three.js提供了两个PBR材质相关的APIMeshStandardMaterial和
MeshPhysicalMaterial,其中MeshPhysicalMaterial是MeshStandardMaterial扩展的子类,提供了更多的功能属性。
金属度属性.metalness表示材质像金属的程度, 非金属材料,如木材或石材,使用0.0,金属使用1.0。
new THREE.MeshStandardMaterial({
metalness: 1.0,//金属度属性
})
粗糙度roughness表示模型表面的光滑或者说粗糙程度,越光滑镜面反射能力越强,越粗糙,表面镜面反射能力越弱,更多地表现为漫反射。
new THREE.MeshStandardMaterial({
roughness: 0.5,//表面粗糙度
})
通过前面学习大家知道,通过纹理贴图加载器TextureLoader的.load()
方法加载一张图片可以返回一个纹理对象Texture
。
立方体纹理加载器CubeTextureLoader的.load()
方法是加载6张图片,返回一个立方体纹理对象TextureLoader。
立方体纹理对象CubeTextureLoader的父类是纹理对象Texture。
// 加载环境贴图
// 加载周围环境6个方向贴图
// 上下左右前后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('./环境贴图/环境贴图0/')
.load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']);
// CubeTexture表示立方体纹理对象,父类是纹理对象Texture
// 加载环境贴图
const textureCube = new THREE.CubeTextureLoader()
.setPath('./环境贴图/环境贴图0/')
.load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']);
new THREE.MeshStandardMaterial({
metalness: 1.0,
roughness: 0.5,
envMap: textureCube, //设置pbr材质环境贴图
})
obj.material.envMap = textureCube; //设置环境贴图
MeshStandardMaterial的.envMapIntensity属性主要用来设置模型表面反射周围环境贴图的能力,或者说环境贴图对模型表面的影响能力。
obj.material.envMapIntensity = 1.0;
MeshPhysicalMaterial和MeshStandardMaterial都是拥有金属度metalness、粗糙度roughness属性的PBR材质,MeshPhysicalMaterial是在MeshStandardMaterial基础上扩展出来的子类,除了继承了MeshStandardMaterial的金属度、粗糙度等属性,还新增了清漆.clearcoat、透光率.transmission、反射率.reflectivity、光泽.sheen、折射率.ior等等各种用于模拟生活中不同材质的属性。
.clearcoat
清漆层属性.clearcoat
可以用来模拟物体表面一层透明图层,就好比你在物体表面刷了一层透明清漆,喷了点水。.clearcoat的范围0到1,默认0。
const material = new THREE.MeshPhysicalMaterial( {
clearcoat: 1.0,//物体表面清漆层或者说透明涂层的厚度
} );
.clearcoatRoughness
清漆层粗糙度.clearcoatRoughness
属性表示物体表面透明涂层.clearcoat
对应的的粗糙度,.clearcoatRoughness
的范围是为0.0至1.0。默认值为0.0。
const material = new THREE.MeshPhysicalMaterial( {
clearcoat: 1.0,//物体表面清漆层或者说透明涂层的厚度
clearcoatRoughness: 0.1,//透明涂层表面的粗糙度
} );
.transmission
为了更好的模拟玻璃、半透明塑料一类的视觉效果,可以使用物理透明度.transmission
属性代替Mesh普通透明度属性.opacity
。
使用.transmission
属性设置Mesh透明度,即便完全透射的情况下仍可保持高反射率。
物理光学透明度.transmission
的值范围是从0.0到1.0。默认值为0.0。
const mesh = gltf.scene.getObjectByName('玻璃01')
mesh.material = new THREE.MeshPhysicalMaterial({
transmission: 1.0, //玻璃材质透光率,transmission替代opacity
})
.ior
非金属材料的折射率从1.0到2.333。默认值为1.5。
new THREE.MeshPhysicalMaterial({
ior:1.5,//折射率
})
// 先配置webgl渲染器开启画布保存
const renderer = new THREE.WebGLRenderer({
//想把canvas画布上内容下载到本地,需要设置为true
preserveDrawingBuffer:true,
});
// 鼠标单击id为download的HTML元素,threejs渲染结果以图片形式下载到本地
document.getElementById('download').addEventListener('click',function(){
// 创建一个超链接元素,用来下载保存数据的文件
const link = document.createElement('a');
// 通过超链接herf属性,设置要保存到文件中的数据
const canvas = renderer.domElement; //获取canvas对象
link.href = canvas.toDataURL("image/png");
link.download = 'threejs.png'; //下载文件名
link.click(); //js代码触发超链接元素a的鼠标点击事件,开始下载文件到本地
})
在开发中,如果有两个重合的矩形平面Mesh,通过浏览器预览,当你旋转三维场景的时候,你会发现模型渲染的时候产生闪烁。
这种现象,主要是两个Mesh重合,电脑GPU分不清谁在前谁在后,这种现象,可以称为深度冲突Z-fighting。
改变Mesh的位置,拉开两个mesh的距离,使其不重合
mesh2.position.z = 1;
在项目开发时,有很多大型3D模型加载的时候会非常慢,这时候往往需要加一个进度条,表示模型的加载进度。
模型加载器的.load()方法,参数一为模型路径;参数二为一个函数,加载结束调用;参数也三为一个函数,每当模型加载部分内容,该函数就会被调用,一次加载过程中一般会被调用多次,直到模型加载完成。jiu
const percentDiv = document.getElementById("per"); // 获取进度条元素
loader.load("../工厂.glb", function (gltf) {
model.add(gltf.scene);
// 加载完成,隐藏进度条
// document.getElementById("container").style.visibility ='hidden';
document.getElementById("container").style.display = 'none';
}, function (xhr) {
// 控制台查看加载进度xhr
// 通过加载进度xhr可以控制前端进度条进度
const percent = xhr.loaded / xhr.total;
console.log('加载进度' + percent);
percentDiv.style.width = percent * 400 + "px"; //进度条元素长度
percentDiv.style.textIndent = percent * 400 + 5 + "px"; //缩进元素中的首行文本
// Math.floor:小数加载进度取整
percentDiv.innerHTML = Math.floor(percent * 100) + '%'; //进度百分比
})
.setFromPoints()是几何体BufferGeometry的一个方法,通过该方法可以把数组pointsArr中坐标数据提取出来赋值给几何体。具体说就是把pointsArr里面坐标数据提取出来,赋值给geometry.attributes.position属性
const pointsArr = [
// 三维向量Vector3表示的坐标值
new THREE.Vector3(0,0,0),
new THREE.Vector3(0,100,0),
new THREE.Vector3(0,100,100),
new THREE.Vector3(0,0,100),
];
// 把数组pointsArr里面的坐标数据提取出来,赋值给`geometry.attributes.position`属性
geometry.setFromPoints(pointsArr);
threejs提供了很多常用的曲线或直线API,可以直接使用。这些API曲线都有一个共同的父类Curve
.getPoints
() 提取曲线的顶点数据
//getPoints是基类Curve的方法,平面曲线会返回一个vector2对象作为元素组成的数组
const pointsArr = arc.getPoints(50); //分段数50,返回51个顶点
console.log('曲线上获取坐标',pointsArr);
.getSpacedPoints() 也是提取曲线的顶点数据,不同的是getSpacedPoints()方法是按照曲线长度等距来获取顶点数据,而getPoints()会考虑到斜率的变化,斜率变化快的位置顶点会更密集
对于一些不规则的曲线,很难用圆弧去描述,可以使用threejs中的样条曲线和贝尔曲线来实现
三维样条曲线CatmullRomCurve3
// 三维样条曲线CatmullRomCurve3,参数是三维向量对象Vector3构成的数组
const arr = [
new THREE.Vector3(-50, 20, 90),
new THREE.Vector3(-10, 40, 40),
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(60, -60, 0),
new THREE.Vector3(70, 0, 80)
]
// 三维样条曲线
const curve = new THREE.CatmullRomCurve3(arr);
//曲线上获取点
const pointsArr = curve.getPoints(100);
const geometry = new THREE.BufferGeometry();
//读取坐标数据赋值给几何体顶点
geometry.setFromPoints(pointsArr);
// 线材质
const material = new THREE.LineBasicMaterial({
color: 0x00fffff
});
// 线模型
const line = new THREE.Line(geometry, material);
二维样条曲线SplineCurve
// 二维向量Vector2创建一组顶点坐标
const arr = [
new THREE.Vector2(-100, 0),
new THREE.Vector2(0, 30),
new THREE.Vector2(100, 0),
];
// 二维样条曲线
const curve = new THREE.SplineCurve(arr);
// 二维二次贝塞尔曲线,参数为三个二维向量对象
// p2为曲线的控制点
const p1 = new THREE.Vector2(-80, 0);
const p2 = new THREE.Vector2(20, 100);
const p3 = new THREE.Vector2(80, 0);
const curve = new THREE.QuadraticBezierCurve(p1, p2, p3);
// 三维二次贝赛尔曲线,参数为三个三维向量对象
// p2为曲线的控制点
const p1 = new THREE.Vector3(-80, 0, 0);
const p2 = new THREE.Vector3(20, 100, 0);
const p3 = new THREE.Vector3(80, 0, 100);
const curve = new THREE.QuadraticBezierCurve3(p1, p2, p3);
// 二维三次贝赛尔曲线,参数为四个二维向量对象
// p1、p4是曲线起始点,p2、p3是曲线的控制点
const p1 = new THREE.Vector2(-80, 0);
const p2 = new THREE.Vector2(-40, 50);
const p3 = new THREE.Vector2(50, 50);
const p4 = new THREE.Vector2(80, 0);
const curve = new THREE.CubicBezierCurve(p1, p2, p3, p4);
// 三维三次贝赛尔曲线,参数为四个三维向量对象
// p1、p4是曲线起始点,p2、p3是曲线的控制点
const p1 = new THREE.Vector3(-80, 0, 0);
const p2 = new THREE.Vector3(-40, 50, 0);
const p3 = new THREE.Vector3(50, 50, 0);
const p4 = new THREE.Vector3(80, 0, 100);
const curve = new THREE.CubicBezierCurve3(p1, p2, p3, p4);
通过CurvePath对象,你可以将直线、圆弧、贝塞尔曲线等线条连接成一条直线
// LineCurve3线段的参数为三维向量对象
new THREE.LineCurve3(new THREE.Vector3(), new THREE.Vector3());
// LineCurve3线段的参数为二维向量对象
new THREE.LineCurve(new THREE.Vector2(), new THREE.Vector2());
下面创建二条直线和一条圆弧并将其使用CurvePath对象连接起来
const R = 80;//圆弧半径
const H = 200;//直线部分高度
// 直线1
const line1 = new THREE.LineCurve(new THREE.Vector2(R, H), new THREE.Vector2(R, 0));
// 圆弧
const arc = new THREE.ArcCurve(0, 0, R, 0, Math.PI, true);
// 直线2
const line2 = new THREE.LineCurve(new THREE.Vector2(-R, 0), new THREE.Vector2(-R, H));
// CurvePath创建一个组合曲线对象
const CurvePath = new THREE.CurvePath();
//line1, arc, line2拼接出来一个U型轮廓曲线,注意顺序
CurvePath.curves.push(line1, arc, line2);
管道几何体就是基于一个3D曲线路径,生成一个管道几何体
构造函数格式:TubeGeometry(path, tubularSegments, radius, radiusSegments, closed)
path 扫描路径,路径要用三维曲线
tubularSegments 路径方向细分数,默认64
radius 管道半径,默认1
radiusSegments 管道圆弧细分数,默认8
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);
车削几何体可以利用一个2D轮廓,将其旋转成一个3D的几何体曲面
格式:LatheGeometry(points, segments, phiStart, phiLength)
points Vector2表示的坐标数据组成的数组
segments 圆周方向细分数,默认12
phiStart 开始角度,默认0
phiLength 旋转角度,默认2π
// Vector2表示的三个点坐标,三个点构成的轮廓相当于两端直线相连接
const pointsArr = [
new THREE.Vector2(50, 60),
new THREE.Vector2(25, 0),
new THREE.Vector2(50, -60)
];
// LatheGeometry:pointsArr轮廓绕y轴旋转生成几何体曲面
// pointsArr:旋转几何体的旋转轮廓形状
const geometry = new THREE.LatheGeometry(pointsArr);
有些时候已知一个多边形的外轮廓坐标,想通过这些外轮廓坐标生成一个多边形几何体平面,这时候你可以借助threejs提供的轮廓填充shapeGeometry几何体实现。
// 一组二维向量表示一个多边形轮廓坐标
const pointsArr = [
new THREE.Vector2(-50, -50),
new THREE.Vector2(-60, 0),
new THREE.Vector2(0, 50),
new THREE.Vector2(60, 0),
new THREE.Vector2(50, -50),
]
// Shape表示一个平面多边形轮廓,参数是二维向量构成的数组pointsArr
const shape = new THREE.Shape(pointsArr);
拉伸ExtrudeGeometry像轮廓几何体一样,都是基于一个平面轮廓shape进行变化,生成一个几何体
// 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
}
);
通过ExtrudeGeometry除了可以实现拉伸成型,也可以让一个平面轮廓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//沿着路径细分精度,越大越光滑
}
);
多边形轮廓shape,是直接通过一组二维向量Vector2表示的xy点坐标创建
shape的父类是path,有以下方法
// 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),
]);
// .currentPoint属性字面意思是当前点,默认值Vector2(0,0)。
const shape = new THREE.Shape();
console.log('currentPoint',shape.currentPoint)
// .moveTo()方法可以改变当前currentPoint属性
const shape = new THREE.Shape();
shape.moveTo(10,0);
// .lineTo()绘制直线线段,直线线段的起点是当前点属性.currentPoint表示的位置,结束点是.lineTo()的参数表示的坐标。
const shape = new THREE.Shape();
//.currentPoint变为(10,0)
shape.moveTo(10,0);
// 绘制直线线段,起点(10,0),结束点(100,0)
shape.lineTo(100,0);
有些多边形是有内孔的,这时候需要借助shape的.holes()来实现
// 创建shape,绘制一个矩形轮廓
const shape = new THREE.Shape();
// .lineTo(100, 0)绘制直线线段,线段起点:.currentPoint,线段结束点:(100,0)
shape.lineTo(100, 0);
shape.lineTo(100, 100);
shape.lineTo(0, 100)
// 创建Shape内孔的轮廓
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);
// 将内孔轮廓添加到shape中
shape.holes.push(path1, path2,path3);
// 使用ExtrudeGeometry拉伸成长方体,方便观看内孔效果
const geometry = new THREE.ExtrudeGeometry(shape, {
depth:20,//拉伸长度
bevelEnabled:false,//禁止倒角
curveSegments:50,
});
借助EdgesGeometry,可以展示出模型的边界线
const geometry = new THREE.BoxGeometry(50, 50, 50);
const material = new THREE.MeshLambertMaterial({
color: 0x004444,
transparent:true,
opacity:0.5,
});
const mesh = new THREE.Mesh(geometry, material);
// 长方体geometry作为EdgesGeometry参数创建一个新的几何体
const edges = new THREE.EdgesGeometry(geometry);
const edgesMaterial = new THREE.LineBasicMaterial({
color: 0x00ffff,
})
// 使用LineSegments将几何体边界线显示出来
const line = new THREE.LineSegments(edges, edgesMaterial);
mesh.add(line);
曲线颜色渐变
通过几何体顶点颜色.attributes.color数据,可以实现曲线的渐变
//创建一个几何体对象
const geometry = new THREE.BufferGeometry();
// 三维样条曲线
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(-50, 20, 90),
new THREE.Vector3(-10, 40, 40),
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(60, -60, 0),
new THREE.Vector3(70, 0, 80)
]);
const pointsArr = curve.getSpacedPoints(100); //曲线取点
geometry.setFromPoints(pointsArr); //pointsArr赋值给顶点位置属性
const pos = geometry.attributes.position;
const count = pos.count; //顶点数量
// 计算每个顶点的颜色值
const colorsArr = [];
for (let i = 0; i < count; i++) {
const percent = i / count; //点索引值相对所有点数量的百分比
//根据顶点位置顺序大小设置颜色渐变
// 红色分量从0到1变化,蓝色分量从1到0变化
colorsArr.push(percent, 0, 1 - percent); //蓝色到红色渐变色
}
//类型数组创建顶点颜色color数据
const colors = new Float32Array(colorsArr);
// 设置几何体attributes属性的颜色color属性
geometry.attributes.color = new THREE.BufferAttribute(colors, 3);
const material = new THREE.LineBasicMaterial({
vertexColors: true, //使用顶点颜色渲染
});
const line = new THREE.Line(geometry, material);
颜色渐变插值方法有.lerpColor()和.lerp(),他们功能是相同的,区别是使用的语法不一样
// lerpColors用法,percent是两颜色的混合百分比
.lerpColors(Color1,Color2, percent)
// lerp用法
const c1 = new THREE.Color(0xff0000); //红色
const c2 = new THREE.Color(0x0000ff); //蓝色
c1.lerp(c2, percent);
const geometry = new THREE.BufferGeometry(); //创建一个几何体对象
// 三维样条曲线
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(-50, 20, 90),
new THREE.Vector3(-10, 40, 40),
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(60, -60, 0),
new THREE.Vector3(70, 0, 80)
]);
const pointsArr = curve.getSpacedPoints(100); //曲线取点
geometry.setFromPoints(pointsArr); //pointsArr赋值给顶点位置属性
const pos = geometry.attributes.position;
const count = pos.count; //顶点数量
// 计算每个顶点的颜色值
const colorsArr = [];
// 根据顶点距离起点远近进行颜色插值计算
const c1 = new THREE.Color(0x00ffff); //曲线起点颜色 青色
const c2 = new THREE.Color(0xffff00); //曲线结束点颜色 黄色
for (let i = 0; i < count; i++) {
const percent = i / count; //点索引值相对所有点数量的百分比
//根据顶点位置顺序大小设置颜色渐变
const c = c1.clone().lerp(c2, percent);//颜色插值计算
colorsArr.push(c.r, c.g, c.b);
}
//类型数组创建顶点颜色color数据
const colors = new Float32Array(colorsArr);
// 设置几何体attributes属性的颜色color属性
geometry.attributes.color = new THREE.BufferAttribute(colors, 3);
const material = new THREE.LineBasicMaterial({
vertexColors: true, //使用顶点颜色渲染
});
const line = new THREE.Line(geometry, material);
创建关键帧动画
// 时间数组
const timeArr = [0, 3, 6, 9];
// 位置数组,3组为一个位置
const positionArr = [0, 0, 0, 100, 0, 0, 0, 0, 100, 0, 0, 0];
// 创建位置关键帧
const posKF = new THREE.KeyframeTrack('Box.position', timeArr, positionArr);
// 创建颜色关键帧
const colorKF = new THREE.KeyframeTrack('Box.material.color', [0, 3, 6, 9], [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]);
// 创建动画
const clip = new THREE.AnimationClip('test', 9, [posKF, colorKF]);
// 创建动画播放器AnimationMixer,设置要播放的物体为mesh
const mixer = new THREE.AnimationMixer(mesh);
// 定义播放器所播放的动画
const clipAction = mixer.clipAction(clip);
// 开始播放动画,play默认为循环播放
clipAction.play();
// 创建时钟对象
const clock = new THREE.Clock();
function loop () {
requestAnimationFrame(loop);
// 获取loop()执行间隔时间
const frameT = clock.getDelta();
// 更新播放器时间数据
mixer.update(frameT);
}
loop();
执行动画播放器AnimationMixer的clipAction()方法会返回一个AnimationAction对象。AnimationAction对象的功能就是用来控制如何播放关键帧动画,比如是否播放、几倍速播放、是否循环播放、是否暂停播放等等
// 设置动画不循环播放
clipAction.loop = THREE.LoopOnce;
// 设置物体状态停留在动画结束的时候(默认情况下物体状态会停留在动画播放前)
clipAction.clampWhenFinished = true;
// 设置动画停止结束,回到开始状态
clipAction.stop();
// 设置动画为暂停播放状态
clipAction.paused = true;
// 设置动画2倍速播放
clipAction.timeScale = 2;
// 通过GUI对象动态调节播放倍速
const gui = new GUI(); //创建GUI对象
gui.add(clipAction, 'timeScale', 0, 6); // 0~6倍速之间调节
// 设置开始播放时间
clipAction.time = 1;
// 设置播放结束时间
clipAction.duration = 5;
// 设置拖动条控制播放动画
import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
const gui = new GUI(); //创建GUI对象
gui.add(clipAction, 'time', 0, 6);
gui.add(clipAction, 'time', 0, 6).step(0.1);
在开发过程中,有时候会用三维建模软件设置动画,一起跟随模型导出文件,这时候,只需要播放文件中的动画即可
loader.load("../士兵.glb", function (gltf) {
// console.log('动画数据', gltf.animations);
model.add(gltf.scene);
// 创建动画播放器
const mixer = new THREE.AnimationMixer(gltf.scene);
// 添加文件中的动画
const clipAction = mixer.clipAction(gltf.animations[3]);
// 开始播放
clipAction.play();
const clock = new THREE.Clock();
function loop () {
requestAnimationFrame(loop);
//clock.getDelta()方法获得loop()两次执行时间间隔
const frameT = clock.getDelta();
// 更新播放器相关的时间
mixer.update(frameT);
}
loop();
})
使用BufferGeometry的morphAttributes属性设置几何体目标顶点变形数据,Mesh的morphTargetInfluences属性控制变形的权重系数
const geometry = new THREE.BoxGeometry(50, 50, 50);
const target1 = new THREE.BoxGeometry(50, 200, 50).attributes.position;
const target2 = new THREE.BoxGeometry(10, 50, 10).attributes.position;
// 设置morphAttributes变形属性为target1和target2
geometry.morphAttributes.position = [target1, target2]
const material = new THREE.MeshLambertMaterial({
color: 0x00ffff
});
const mesh = new THREE.Mesh(geometry, material);
mesh.name = 'Box';
// 将变形属性创建成关键帧动画
const KF1 = new THREE.KeyframeTrack('Box.morphTargetInfluences[0]', [0, 5], [0, 1]);
const KF2 = new THREE.KeyframeTrack('Box.morphTargetInfluences[1]', [5, 10], [0, 1]);
const clip = new THREE.AnimationClip("test", 10, [KF1, KF2]);
// 播放变形动画
const mixer = new THREE.AnimationMixer(mesh);
const clipAction = mixer.clipAction(clip);
clipAction.play();
clipAction.loop = THREE.LoopOnce; //不循环播放
clipAction.clampWhenFinished = true // 物体状态停留在动画结束的时候
const clock = new THREE.Clock();
function loop () {
requestAnimationFrame(loop);
const frameT = clock.getDelta();
// 更新播放器时间
mixer.update(frameT);
}
loop();
export default mesh;
Bone骨骼是threejs的一个类,用来模拟人或动物的骨骼,他的父节点是Object3D,继承了位置属性position、旋转属性rotation等等
// 创建骨骼
const Bone1 = new THREE.Bone();
const Bone2 = new THREE.Bone();
const Bone3 = new THREE.Bone();
// 设置骨骼父子关系,将骨骼关联起来
Bone1.add(Bone2);
Bone2.add(Bone3);
Bone1.position.set(50, 0, 50);
Bone2.position.y = 60;
Bone3.position.y = 30;
// 将骨骼添加至组对象中
const group = new THREE.Group();
group.add(Bone1);
// 创建骨骼处理器,显示我们创建的骨骼对象
const SkeletonHelper = new THREE.SkeletonHelper(group);
group.add(SkeletonHelper);
// GUI动态控制骨骼旋转
const gui = new GUI();
gui.add(Bone1.rotation, 'x', 0, Math.PI / 2).name('骨骼1');
gui.add(Bone2.rotation, 'x', 0, Math.PI / 2).name('骨骼2');
tweenjs是一个JavaScript编写的补间动画库,如果你使用three.js开发web3d项目,使用tween.js辅助three.js生成动画效果也是比较好的选择。
npm i @tweenjs/tween.js@^18
import TWEEN from '@tweenjs/tween.js';
tweenjs从语法角度上讲,就是改变自己的参数对象
const geometry = new THREE.BoxGeometry(10, 10, 10);
const material = new THREE.MeshLambertMaterial({
color: 0x00ffff,
});
const mesh = new THREE.Mesh(geometry, material);
//创建一段mesh平移的动画
const tween = new TWEEN.Tween(mesh.position);
//经过2000毫秒,pos对象的x和y属性分别从零变化为100、50
tween.to({x: 100,y: 50}, 2000);
//tween动画开始执行
tween.start();
// 渲染循环
function render() {
// 循环更新TWEEN数据
TWEEN.update();
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
const R = 150;
new TWEEN.Tween({ angle: 0 }) // 给定一个开始参数angle,为0
.to({ angle: Math.PI * 2 }, 10000) // 结束时angle为360度,用时10秒
.onUpdate(function (obj) { // tweenjs数据时更新执行此函数
camera.position.x = R * Math.cos(obj.angle);
camera.position.z = R * Math.sin(obj.angle);
camera.lookAt(0, 0, 0);
})
.start();
// 渲染循环
function render () {
TWEEN.update();
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
tweenjs回调函数有onStart()、onUpdate()、onComplete()三种
在动画播放的时候,默认是匀速状态的,在项目开发中,如果在动画播放的某一段进行一个缓慢、缓冲的效果,将会看起来更加自然
常见的算法名称有如下
每个算法又有三种缓动方式
示例如下
// 创建tweenjs动画,改变threejs相机位置
new TWEEN.Tween(camera.position)
.to({
x: 300,
y: 300,
z: 300,
}, 3000)
// 二次方缓动算法,先快后慢
.easing(TWEEN.Easing.Quadratic.Out)
.start();