目录
了解threejs
开门上threejs官网
- threejs和webgl区别
首先两者都是用于创建 3D 图形和交互性的技术,但在使用,生态和性能上有所区别:
(1)使用上,webgl更底层更抽象,它可直接访问图形硬件,编写底层图形渲染代码(如,着色器,矩阵变换,顶点缓冲区,光源等);threejs更易使用,它建于webgl之上,专注创建3d内容而不是处理底层的图形编程。
(2)生态上,webgl可查阅的文档资源较少;threejs有官网案例,开源项目还有辅助工具(如GUI,Tween,相机控件)生态更庞大些。
(3)性能上,如果对性能有严格要求,可直接使用webgl更精细的控制图片渲染流程,需要投入更多专业知识和时间;threejs对一些复杂场景做了一些优化和抽象,通常能提供足够的性能。
总之,webgl 更适合那些对图形编程有深入了解和对性能有更高要求的开发者,threejs易学易使用,专注3d内容但没有深入了解图形编程的人,根据需求和技术各取所需。
- 基本使用流程
创建场景scene → 创建一个相机camera,设置相机位置 → 创建一个渲染器canvas,尺寸 → 创建几何体geometry,添加材质material → 添加灯光light → 把要展示的添加到场景里并渲染
复习3d三要素:视点(相机/眼睛),目标(几何体),上方向
(官网基本场景案例)
以下是常用光源、材质、几何体属性合集
const itemType = {
SpotLight: ['color', 'intensity', 'distance', 'angle', 'exponent'], // 聚光灯
AmbientLight: ['color'], // 环境光
PointLight: ['color', 'intensity', 'distance'], // 点光源
DirectionalLight: ['color', 'intensity'], // 平行光
HemisphereLight: ['skyColor', 'groundColor', 'intensity'], // 半球光
MeshBasicMaterial: ['color', 'opacity', 'transparent', 'wireframe', 'visible'], // 基础,显示简单颜色。
MeshDepthMaterial: ['wireframe', 'cameraNear', 'cameraFar'], // 深度,指与相机距离越远越暗
MeshNormalMaterial: ['opacity', 'transparent', 'wireframe', 'visible', 'side'], // 法向量,把法向量映射到RGB颜色的材质
MeshLambertMaterial: ['opacity', 'transparent', 'wireframe', 'visible', 'side', 'ambient', 'emissive', 'color'], // 郎伯,良好暗淡效果,没有镜面高光
MeshPhongMaterial: ['opacity', 'transparent', 'wireframe', 'visible', 'side', 'ambient', 'emissive', 'color', 'specular', 'shininess'], //phong,有镜面高光
ShaderMaterial: ['red', 'alpha'], // 着色器,可自定义应用所有光照场景
LineBasicMaterial: ['color'], // 实线
LineDashedMaterial: ['dashSize', 'gapSize'], // 虚线
PlaneGeometry: ['width', 'height', 'widthSegments', 'heightSegments'], // 平面
PlaneBufferGeometry: ['width', 'height', 'widthSegments', 'heightSegments'], // 缓冲,顶点数据索引缓存,有效减少向 GPU 传输
CircleGeometry: ['radius', 'segments', 'thetaStart', 'thetaLength'], // 圆
BoxGeometry: ['width', 'height', 'depth', 'widthSegments', 'heightSegments', 'depthSegments'], // 矩形
SphereGeometry: ['radius', 'widthSegments', 'heightSegments', 'phiStart', 'phiLength', 'thetaStart', 'thetaLength'], // 球
CylinderGeometry: ['radiusTop', 'radiusBottom', 'height', 'radialSegments', 'heightSegments', 'openEnded'], // 圆柱
TorusGeometry: ['radius', 'tube', 'radialSegments', 'tubularSegments', 'arc'], // 圆环
TorusKnotGeometry: ['radius', 'tube', 'radialSegments', 'tubularSegments', 'p', 'q', 'heightScale'], // 扭结
PolyhedronGeometry: ['radius', 'detail'], // 多面体
TetrahedronGeometry: ['radius', 'detail'], // 四面体
OctahedronGeometry: ['radius', 'detail'], // 八面体
IcosahedronGeometry: ['radius', 'detail'], //二十面体
TextGeometry: ['size', 'bevelThickness', 'bevelSize', 'bevelEnabled', 'bevelSegments', 'curveSegments', 'steps'], // 文字
设置阴影
设置对光有反应的材质 → 开启几何体阴影 → 使用平面接收 → 开启灯光阴影
(1)不是所有材质都对光有反应
(2)不是所有光都产生阴影
(1)对光有反应的材质:郎伯材质MeshLambertMaterial,MeshPhongMaterial,MeshStandardMaterial,MeshPhysicalMaterial
(2)可产生明确阴影的光:点光源SpotLight,平行光Directionallight
// 几何开启阴影
cube.castShadow = true;
// 使用平面接收阴影
plane.receiveShadow = true;
// 设置灯光开启阴影
spotLight.castShadow = true;
renderer.shadowMapEnabled = true;
spotLight.shadowMapWidth = 4096;//使阴影更清晰
加载外部文件
需要添加相关文件加载器xxxLoader
const loader = new THREE.OBJMTLLoader()
loader.load('../assets/models/city.obj', '../assets/models/city.mtl', (mesh) => {
scene.add(mesh);
});
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
const fbxLoader = new THREE.FBXLoader();
fbxLoader.load('urlxxx.fbx', (object) => {
// 这里object是group对象
// 遍历场景中的所有几何体数据
object.traverse((child) => {
if (child.isMesh) {
// 对模型数据进行后期二次处理
// scene.add(child);
}
});
}
补充:组对象Group、层级对象
(1)创建var group = new THREE.Group();
(2)添加group.add(mesh1);
(3)查看子对象group.children
(4)删除group.remove(mesh1);
(5)遍历group.traverse((child)=>{});
使用GUI绘制控制面板
辅助工具,在动画中为属性赋值对应的变量
//需要控制的属性
const controls = {
color: '', // 是否要组合成立方体
width: '',
};
let geo;
const gui = new dat.GUI();
for (const key in controls) {
if(key=='color'){
gui.addColor(controls, 'color',key).onChange((value) => {
controls.color = value;
});
}else{
gui.add(controls, key).onChange(() => {
// 更新几何体属性,width等尺寸需要如下操作
//1. 先删除
scene.remove(geo);
//2. 再添加
const geo = new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10, 10, 10, 10), new THREE.MeshNormalMaterial())
scene.add(geo);
});
}
}
Tweenjs动画
优点:
- 支持多种动画类型:Tween.js 可以创建各种类型的动画效果,包括数字变化、颜色渐变、位置移动、缩放变换等。你可以对不同的属性进行动画化,实现更丰富多样的效果。Tween.js 还提供了丰富的缓动函数(easing functions),可帮助你实现自定义的动画变化曲线。
- 时间控制和事件回调:Tween.js 允许你控制动画的开始、暂停、恢复和停止。你可以根据需要随时进行时间控制,以适应交互或其他场景的需求。此外,Tween.js 还支持在动画达到特定时间点或完成时触发回调函数,以便执行进一步的操作或处理事件。
new TWEEN.Tween(cube.rotation).to({
x: cube.rotation.x + 2,
y: cube.rotation.y + 2,
z: cube.rotation.z + 2,
}, 2000).start().repeat(Infinity);
//动画渲染/循环
function animate() {
// cube.rotation.x += 0.01;
// cube.rotation.y += 0.01;
TWEEN.update();
// 渲染
renderer.render(scene, camera);
requestAnimationFrame(animate);//浏览器下次重绘之前执行回调
}
animate();
相机控件
- 这里就说常用的Orbitcontrols(轨道控制器),可以使得相机围绕目标进行轨道运动,可以通过鼠标多方面拖拽观察模型。
// 相机(透视)
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 100000);
camera.position.set(1000, 500, 100);
scene.add(camera);
// 添加相机控件-轨迹
const controls = new OrbitControls(camera, canvas);
// 是否有惯性
controls.enableDamping = true;
// 是否可以缩放
// controls.enableZoom = true;
controls.enableZoom = false; // 采用鼠标滚轮
注意: 采用鼠标滚轮,一定把相机控件的缩放关闭controls.enableZoom = false;
- 让场景根据鼠标位置进行缩放
思路主要以下:鼠标桌标计算、坐标转换unproject、统一化normalize、改变相机中心点
addWheel() {
const body = document.body;
body.onmoussewheel = (event) => {
const value = 30;
//获取鼠标坐标位置
const x = (event.clientX / window.innerWidth) * 2 - 1;
const y = -(event.clientY / window.innerHeight) * 2 + 1;
// 获取屏幕坐标
const vector = new THREE.Vector3(x, y, 0.5);
// 将屏幕坐标转换为three.js场景坐标(鼠标点击位坐标置转三维坐标)
vector.unproject(this.camera);
// 获取缩放的坐标信息
vector.sub(this.camera.position).normalize();
if (event.wheelDelta > 0) {
//针对相机做处理
this.camera.position.x += vector.x * value;
this.camera.position.y += vector.y * value;
this.camera.position.z += vector.z * value;
controls.target.x += vector.x * value;
controls.target.y += vector.y * value;
controls.target.z += vector.z * value;
} else {
this.camera.position.x -= vector.x * value;
this.camera.position.y -= vector.y * value;
this.camera.position.z -= vector.z * value;
controls.target.x -= vector.x * value;
controls.target.y -= vector.y * value;
controls.target.z -= vector.z * value;
}
}
}
一些效果
雾化-fog
scene.fog = new THREE.Fog(0xffffff, 1, 50);
辉光-后期处理通道 pass
RenderPass二次处理 → OutlinePass配置辉光属性 → EffectComposer组合器组合以上通道 → 渲染组合render
//辉光效果
// 创建了一个渲染通道,这个通道会渲染场景,不会渲染到屏幕上
const renderScene = new RenderPass(scene, camera);//对图像做二次处理
// 分辨率 场景 相机 当前选中的物体(需要添加辉光效果)
const outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera, [cube1, cube2])
outlinePass.renderToScreen = true; // 渲染到屏幕上
outlinePass.edgeStrength = 3; // 尺寸
outlinePass.edgeGlow = 2; // 发光的强度
outlinePass.edgeThickness = 2; // 光晕粗细
outlinePass.pulsePeriod = 1;// 闪烁的速度,值越小闪烁越快
outlinePass.visibleEdgeColor.set('yellow');
// 创建一个组合器对象,添加处理通道
const bloom = new EffectComposer(renderer)
bloom.setSize(window.innerWidth, window.innerHeight)
bloom.addPass(renderScene)
bloom.addPass(outlinePass)
//动画渲染/循环
function animate() {
renderer.render(scene, camera);
bloom.render();
requestAnimationFrame(animate);//浏览器下次重绘之前执行回调
}
animate();
反光-环境贴图
创建虚拟场景盒子skybox → 设置几何体cube材质 → cubeCamera获取cube材质renderTarget → 给反光几何添加上反光材质envMap为renderTarget
// 添加轨道控件
const controls = new THREE.OrbitControls(camera)
// 环境纹理,虚拟反光效果
// 创建虚拟的场景
const imgs = [
'./assets/img/sky/right.jpg',
'./assets/img/sky/left.jpg',
'./assets/img/sky/top.jpg',
'./assets/img/sky/bottom.jpg',
'./assets/img/sky/front.jpg',
'./assets/img/sky/back.jpg',
]
const mats = [];
for (let i = 0; i < imgs.length; i++) {
mats.push(new THREE.MeshBasicMaterial({
//bumpmap凹凸,normalmap法向
map: THREE.ImageUtils.loadTexture(imgs[i]),
side: THREE.DoubleSide,// 环境贴图需要设置,默认frontside前外面,backside后内面,doubleside两面
}))
}
// 虚拟环境盒子
const skybox = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 100), new THREE.MeshFaceMaterial(mats))
scene.add(skybox)
// 创建一个球体 和一个立方体
const sphereGeometry = new THREE.SphereGeometry(4, 15, 15);
const cubeGeometry = new THREE.BoxGeometry(5, 5, 5);
// 立方体贴图是和环境一致, 球体是跟随当前环境
const cubeMaterial = new THREE.MeshBasicMaterial({
envMap: THREE.ImageUtils.loadTextureCube(imgs)//使用和天空盒子一样材质
})
// 通过立方体相机来实现
const cubeCamera = new THREE.CubeCamera(0.1, 2000, 256);
scene.add(cubeCamera);
const sphereMaterial = new THREE.MeshBasicMaterial({
envMap: cubeCamera.renderTarget,
})
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
sphere.position.x = 5;
cube.position.x = -5;
scene.add(sphere)
scene.add(cube)
const clock = new THREE.Clock();
//动画渲染/循环
function animate() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
controls.update(clock.getDelta());//动画添加相机更新
// 渲染
renderer.render(scene, camera);
requestAnimationFrame(animate);//浏览器下次重绘之前执行回调
cubeCamera.updateCubeMap(renderer, scene);//反光
}
animate();
渐变色-ShaderMaterial
- 主要使用到一个mix函数,该函数用于对两个值进行线性插值。它的作用是根据一个插值因子(介于0和1之间的值,),在两个输入值之间进行混合。语法:
mix(value1, value2, factor)
value1:第一个输入值。
value2:第二个输入值。
factor:插值因子,控制两个输入值之间的混合比例。取值范围为0到1,其中0表示完全使用 value1,1表示完全使用 value2。
- 自定义着色器ShaderMaterial
名称 | 描述 |
---|---|
vertexShader | 定义顶点着色器(gl_Position,gl_PointSize) |
fragmentShader | 定义片元着色器(gl_FragColor,gl_FragCoord) |
uniforms | 所有顶点都具有相同的值的变量 |
attributes | 只在顶点着色器中,只能声明全局变量 |
varying | 从顶点着色器向片元着色器传递数据 |
transparent | true,使得着色器支持透明 |
depthTest | THREE.DoubleSide,解决建筑物展示部分问题 |
side | true,可被建筑物遮挡隐藏 |
- - - | - - - |
const material = new THREE.ShaderMaterial({
uniforms: {
u_city_color: {
// 得需要一个模型颜色 最底部显示的颜色
value: new THREE.Color('#1B3045')
},
u_head_color: {
// 要有一个头部颜色 最顶部显示的颜色
value: new THREE.Color('#ffffff')
},
u_size: {
value: 100,
},
},
vertexShader: `
varying vec3 v_position;
void main() {
v_position = position;
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(v_position, 1.0);
}
`,
fragmentShader: `
varying vec3 v_position;
uniform vec3 u_city_color;
uniform vec3 u_head_color;
uniform float u_size;
void main() {
vec3 base_color = u_city_color;
base_color = mix(base_color, u_head_color, v_position.z / u_size);
gl_FragColor = vec4(base_color, 1.0);
}
`,
})
补充实现渐变效果有哪些:
(1)css3线性渐变样式
div {background: linear-gradient(45deg, #ff0000, #00ff00);}
(2)渐变图片做贴图
飞线-贝塞尔曲线
- 效果描述:从起点飞一个弧度到终点
- 准备:起点source、终点target、中点center,弧度高度height,飞线长度range、粒子大小size
- 中点获取:通过lerp函数,该函数用于在两个值之间进行线性插值。它的作用是根据一个插值因子,在两个输入值之间生成平滑过渡的值。语法:
lerp(value1, value2, factor)
(参数同mix函数)
// 通过起始点和终止点来计算中心位置
const center = target.clone().lerp(source, 0.5);
// 设置中心位置的高度
center.y += options.height;
- 绘制飞线:使用二次贝塞尔曲线,通过中心点控制飞线
贝塞尔曲线:是一种平滑曲线,由控制点定义。二次由1个控制点,三次由2个控制点,常用于平滑路径动画、形状变形。
- 获取粒子
// 起点到终点的距离,这里是粒子数量
const len = parseInt(source.distanceTo(target));
// 获取粒子
const points = curve.getPoints(len);
- 着色器定义位置Attribute(Float32BufferAttribute)
const positions = [];//粒子坐标集合
const aPositions = [];//粒子索引集合
points.forEach((item, index) => {
positions.push(item.x, item.y, item.z)
aPositions.push(index)
})
const geometry = new THREE.BufferGeometry();//空几何图形
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))//第二参数表示获取数据数量
geometry.setAttribute('a_position', new THREE.Float32BufferAttribute(aPositions, 1))
补充:Geometry和BufferGeometry使用区别
它们都是用于描述和存储3D对象的数据结构。
(1)Geometry,使用js对象来存储几何数据,适用简单模型,优点易创建,缺点就是js对象存储额外消耗内存和cpu。
(2)BufferGeometry,使用TypedArray更底层方式存储几何数据,适用于处理大量顶点数据的复杂场景,优点少内存,缺点相对难创建。
geometry.vertices可获取顶点数据
- 飞线长度:主要作用是控制size和opacity实现拖尾
const material = new THREE.ShaderMaterial({
uniforms: {
u_color: {
value: new THREE.Color(options.color)
},
u_range: {
value: options.range
},
u_size: {
value: options.size
},
//粒子数量
u_total: {
value: len,
},
u_time: this.time,
},
vertexShader: `
attribute float a_position;
uniform float u_time;
uniform float u_size;
uniform float u_range;
uniform float u_total;
varying float v_opacity;
void main() {
float size = u_size;
float total_number = u_total * mod(u_time, 1.0);
if (total_number > a_position && total_number < a_position + u_range) {
// 拖尾效果,超出范围的大小为0
float index = (a_position + u_range - total_number) / u_range;
size *= index;
v_opacity = 1.0;
} else {
v_opacity = 0.0;
}
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size / 10.0;
}
`,
fragmentShader: `
uniform vec3 u_color;
varying float v_opacity;
void main() {
gl_FragColor = vec4(u_color, v_opacity);
}
`,
transparent: true, // 使得着色器支持透明度
});