Three.js 是 "WebGL 的封装库",帮你屏蔽了底层的着色器 / 缓冲区细节,专注于 "3D 场景搭建",开发效率高,是通用 3D 开发的首选。
他的核心是 "场景 - 相机 - 渲染器" 的联动逻辑,先掌握基础组件,再学进阶功能(如自定义着色器)
实践只提供参考代码,自己去找模型尝试。
1.Three.js 核心组件
1.1 三要素:场景(Scene
)、相机(PerspectiveCamera/OrthographicCamera
)、渲染器(WebGLRenderer
)
场景(Scene
)
场景就像是一个虚拟的 "3D 舞台",所有的 3D 物体(比如模型、灯光、粒子等)都需要放在这个舞台上才能被看到。
场景本身不直接显示任何东西,它只是一个容器,负责管理所有需要渲染的对象。在 Three.js 中,你可以通过
add()
方法往场景里添加各种元素。
javascript
// 创建一个场景
const scene = new THREE.Scene();
// 创建一个立方体并添加到场景中
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube); // 将立方体添加到场景
相机(Camera)
相机决定了我们从哪个角度和视角来看场景中的物体。
透视相机(PerspectiveCamera)
这是最常用的相机,模拟人眼观察世界的方式,远处的物体看起来小,近处的物体看起来大,有 "近大远小" 的透视效果。
参数说明(通俗易懂版):
视野角度:相机能看到的范围有多大(比如 90 度)
宽高比:画面的宽度和高度比例(通常是浏览器窗口的宽高比)
近平面:离相机多近的物体才会被看到
远平面:离相机多远的物体还能被看到
javascript// 创建透视相机 const camera = new THREE.PerspectiveCamera( 75, // 视野角度 window.innerWidth / window.innerHeight, // 宽高比 0.1, // 近平面 1000 // 远平面 ); camera.position.z = 5; // 把相机往后移动一点,这样能看到场景中的物体
正交相机(OrthographicCamera)
这种相机没有透视效果,远处和近处的物体看起来一样大,适合 2D 渲染或建筑图纸等需要精确尺寸的场景。
javascript// 创建正交相机 const camera = new THREE.OrthographicCamera( window.innerWidth / -2, // 左边界 window.innerWidth / 2, // 右边界 window.innerHeight / 2, // 上边界 window.innerHeight / -2,// 下边界 0.1, // 近平面 1000 // 远平面 );
渲染器(WebGLRenderer)
渲染器的作用是把场景和相机 "结合" 起来,计算出最终应该显示在屏幕上的图像,并把它绘制到网页的 canvas 元素上。
javascript
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染器的尺寸为窗口大小
document.body.appendChild(renderer.domElement); // 将渲染器生成的canvas元素添加到网页中
// 执行渲染(相当于"拍照")
renderer.render(scene, camera);
总结
-
场景是舞台,存放所有物体
-
相机是观众的眼睛,决定看什么角度
-
渲染器是画笔,把相机看到的场景画在屏幕上
当你想要制作动画时,只需要在一个循环中不断改变场景中物体的位置或相机角度,然后重新调用renderer.render()
方法即可,这就像拍视频一样,连续播放多张照片就形成了动画效果。
1.2 基础元素:几何体(BoxGeometry/SphereGeometry
)、材质(MeshBasicMaterial/MeshStandardMaterial
)、网格(Mesh
)
几何体(BoxGeometry/SphereGeometry
)
几何体就像是物体的 "骨架" 或 "模具",定义了物体的形状和大小,但它本身是没有颜色和质感的。
BoxGeometry(立方体几何体)
可以想象成一个纸箱的框架,有长、宽、高三个维度。创建时需要指定这三个参数,比如
new THREE.BoxGeometry(1, 1, 1)
就创建了一个边长为 1 的正方体框架。
SphereGeometry(球体几何体) :类似一个篮球的内部支架,由无数个小三角形拼接而成。创建时需要指定半径和分段数,分段数越高,球体越光滑,比如
new THREE.SphereGeometry(1, 32, 32)
创建了一个半径为 1 的球体。
材质(MeshBasicMaterial/MeshStandardMaterial
)
材质就像是物体的 "皮肤",决定了物体的颜色、质感、是否反光等外观属性,但它本身没有固定形状。
MeshBasicMaterial(基础网格材质) :
最基础的材质,就像用彩色纸糊出来的效果,不考虑光照影响,永远是平光的。可以设置颜色,比如
new THREE.MeshBasicMaterial({color: 0xff0000})
就是红色的材质。
MeshStandardMaterial(标准网格材质) :更接近真实世界的材质,会受光照影响,能表现出金属、塑料等不同质感。比如可以设置金属度(metalness)和粗糙度(roughness),new THREE.MeshStandardMaterial({color: 0x00ff00, metalness: 0.5, roughness: 0.5})就创建了一个绿色的、半金属质感的材质。
网格(Mesh
)
网格是几何体和材质的 "结合体",就像把皮肤贴在骨架上,形成一个可以显示在屏幕上的具体物体。
javascript
// 创建一个立方体骨架
const geometry = new THREE.BoxGeometry(1, 1, 1);
// 创建一个红色皮肤
const material = new THREE.MeshBasicMaterial({color: 0xff0000});
// 把皮肤贴在骨架上,得到一个红色立方体
const cube = new THREE.Mesh(geometry, material);
javascript
scene.add(cube); // 把物体放入场景
1.3 辅助工具:坐标轴(AxesHelper
)、性能监控(Stats
)
在 Three.js 中,辅助工具就像我们做手工时用的尺子、放大镜一样,能帮我们更方便地调试和观察 3D 场景。
坐标轴(AxesHelper)
在 3D 场景中显示 X、Y、Z 三条坐标轴,帮你判断物体的位置和方向.
javascript
// 创建一个长度为5的坐标轴(数值越大,线越长)
const axesHelper = new THREE.AxesHelper(5);
// 把坐标轴放进场景,就能看到了
scene.add(axesHelper);
性能监控(Stats)
实时显示 3D 场景的运行性能,比如每秒渲染多少帧(FPS).
FPS(Frames Per Second):每秒渲染的画面数量。数值越高,画面越流畅(通常 60 是比较理想的状态)。
如果 FPS 低于 30,画面会感觉卡顿,就像看幻灯片
javascript
// 创建性能监控器
const stats = new Stats();
// 把监控器显示在网页右上角(默认是左上角)
stats.dom.style.position = 'absolute';
stats.dom.style.right = '0px';
stats.dom.style.top = '0px';
// 把监控器添加到网页中
document.body.appendChild(stats.dom);
// 在动画循环中更新数据
function animate() {
requestAnimationFrame(animate);
stats.update(); // 每次刷新画面时,更新性能数据
renderer.render(scene, camera);
}
animate();
1.4 实践:搭建一个 "静态 3D 场景":包含立方体、球体、地面,添加坐标轴和光照。
代码:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>静态3D场景</title>
<!-- 引入Three.js库 -->
<script src="https://cdn.tailwindcss.com"></script>
<link
href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css"
rel="stylesheet"
/>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<style>
body {
margin: 0;
}
canvas {
display: block;
}
.info {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px;
border-radius: 5px;
font-family: Arial, sans-serif;
font-size: 12px;
}
</style>
</head>
<body>
<div class="info">
<p>静态3D场景演示</p>
<p>包含:立方体、球体、地面、坐标轴和光照</p>
<p>红色:X轴 | 绿色:Y轴 | 蓝色:Z轴</p>
</div>
<script>
// 1. 创建场景
// 场景就像一个容器,用来放置所有的3D物体
const scene = new THREE.Scene();
// 设置场景背景颜色
scene.background = new THREE.Color(0xf0f0f0); // 浅灰色背景
// 2. 创建相机
// 透视相机,模拟人眼观察世界的方式
// 参数:视野角度、宽高比、近平面、远平面
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// 设置相机位置
camera.position.z = 10; // 往后移
camera.position.y = 5; // 往上移
camera.position.x = 5; // 往右移
// 让相机看向场景中心
camera.lookAt(scene.position);
// 3. 创建渲染器
// 渲染器负责将3D场景渲染到网页上
const renderer = new THREE.WebGLRenderer();
// 设置渲染器尺寸为窗口大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 将渲染器的DOM元素添加到页面
document.body.appendChild(renderer.domElement);
// 4. 添加坐标轴辅助工具
// 参数是坐标轴的长度
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 5. 添加光照
// 环境光:均匀照亮所有物体,没有方向感
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // 颜色,强度
scene.add(ambientLight);
// 平行光:类似太阳光,有方向,会产生阴影
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(100, 15, 10); // 设置光源位置
scene.add(directionalLight);
// 6. 创建地面
// 地面使用平面几何体
const planeGeometry = new THREE.PlaneGeometry(60, 60); // 宽20,长20
// 使用标准材质,会受光照影响
const planeMaterial = new THREE.MeshStandardMaterial({
color: "gray", // 灰色
side: THREE.DoubleSide, // 让平面两面都可见
});
// 创建网格(几何体+材质)
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
// 旋转地面,让它水平放置
plane.rotation.x = -Math.PI / 2; // 绕X轴旋转-90度
scene.add(plane);
// 7. 创建立方体
// 立方体几何体:参数分别是宽、高、深
const cubeGeometry = new THREE.BoxGeometry(2, 2, 2);
// 使用标准材质
const cubeMaterial = new THREE.MeshStandardMaterial({
color: 0xff0000, // 红色
});
// 创建立方体网格
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
// 设置立方体位置
cube.position.x = -3; // X轴方向
cube.position.y = 1; // Y轴方向(让它立在地面上)
cube.position.z = 0; // Z轴方向
scene.add(cube);
// 8. 创建球体
// 球体几何体:参数分别是半径、水平分段数、垂直分段数
// 分段数越高,球体越光滑
const sphereGeometry = new THREE.SphereGeometry(1.5, 32, 32);
// 使用标准材质
const sphereMaterial = new THREE.MeshStandardMaterial({
color: 0x00ff00, // 绿色
});
// 创建球体网格
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
// 设置球体位置
sphere.position.x = 3; // X轴方向
sphere.position.y = 1; // Y轴方向(让它立在地面上)
sphere.position.z = 0; // Z轴方向
scene.add(sphere);
// 9. 窗口大小变化时调整渲染
window.addEventListener("resize", () => {
// 更新相机的宽高比
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
// 更新渲染器尺寸
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 10. 渲染场景
function render() {
// 使用requestAnimationFrame创建动画循环
requestAnimationFrame(render);
// 渲染场景和相机
renderer.render(scene, camera);
}
// 开始渲染
render();
</script>
</body>
</html>
效果:

2.Three.js 核心能力
2.1模型加载:加载 GLB/GLTF 模型(GLTFLoader
)
什么是 GLB/GLTF 模型?
简单说,它们是 3D 模型的 "文件格式",就像图片有 JPG、PNG 格式,视频有 MP4 格式一样。
GLTF:被称为 "3D 界的 JPG",是一种通用的 3D 模型格式,支持模型、材质、动画等信息。
GLB:是 GLTF 的 "压缩版",把所有模型数据打包成一个文件,加载更快,就像把一本书的所有页装订成一本,而不是散页。
这些模型通常不是用 Three.js 手动创建的(太麻烦了),而是用专业 3D 软件(比如 Blender、Maya)做好后导出的,然后用 Three.js 加载到我们的场景中。
什么是 GLTFLoader?
GLTFLoader 就是 Three.js 提供的 "模型搬运工",专门负责把 GLB/GLTF 格式的模型文件 "搬到" 我们的 3D 场景里。
加载模型的简单步骤
引入 "搬运工"(GLTFLoader)
Three.js 核心库里没有自带这个工具,需要额外引入
html<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/GLTFLoader.js"></script>
创建 "搬运工"
在 JavaScript 中实例化这个工具
javascriptconst loader = new THREE.GLTFLoader();
指定要搬运的模型文件
告诉模型文件在哪里(文件路径)
javascript// 加载GLB模型(如果是GLTF文件,路径换成xxx.gltf即可) loader.load( 'models/robot.glb', // 模型文件的路径(比如你下载的机器人模型) // 加载成功时的操作:把模型放进场景 function(gltf) { // gltf.scene就是加载好的模型整体 scene.add(gltf.scene); // 把模型添加到场景中,这样就能看到了 console.log('模型加载成功!'); }, // 加载过程中的进度提示(可选) function(xhr) { console.log(`加载中... ${(xhr.loaded / xhr.total * 100)}%`); }, // 加载失败时的提示(可选) function(error) { console.log('加载失败:', error); } );
加载后能做什么?
模型加载到场景后,你可以像操作自己创建的立方体、球体一样操作它:
移动位置:
gltf.scene.position.set(1, 2, 3)
旋转:
gltf.scene.rotation.y = Math.PI / 4
(转 45 度)缩放:
gltf.scene.scale.set(0.5, 0.5, 0.5)
(缩小一半)
为什么要用 GLB/GLTF?
通用性强:几乎所有 3D 软件都支持导出这种格式,方便你从网上下载现成模型(比如很多免费模型网站提供 GLB/GLTF 格式)。
加载快:相比其他 3D 格式(比如 OBJ),它体积更小,加载速度更快,适合网页 3D 应用。
2.2 动画控制:AnimationMixer
控制模型动画、Tween.js
实现属性过渡
在 Three.js 中,动画控制能让你的 3D 场景 "动起来",就像给静态的模型赋予生命。我们可以用两个工具实现不同类型的动画:AnimationMixer(控制模型自带的动画)和 Tween.js(实现物体属性的平滑变化)。
AnimationMixer(模型动画控制器)
控制从 3D 建模软件(如 Blender)导出的模型自带的动画(比如人物走路、机器人旋转等)。
把 3D 模型想象成一个 "木偶",建模师在制作时已经给它设计好了一套套动作(比如走路、挥手),这些动作被保存为 "动画片段"。AnimationMixer 就像一个 "木偶师",负责播放、暂停、切换这些现成的动作。
动画片段(AnimationClip):模型中保存的单个动作(比如 "走路" 是一个片段,"跳跃" 是另一个片段)。
混合器(Mixer):管理这些动画片段的播放器,能控制播放速度、切换动作、甚至混合多个动作(比如边走路边挥手)。
javascript// 假设已经加载了一个带动画的模型model // 创建混合器,关联到这个模型 const mixer = new THREE.AnimationMixer(model); // 从模型中获取第一个动画片段(比如"走路") const animationClip = model.animations[0]; // 让混合器播放这个动画片段 const action = mixer.clipAction(animationClip); action.play(); // 开始播放动画 // 在动画循环中更新混合器(让动画动起来) function animate() { requestAnimationFrame(animate); const delta = clock.getDelta(); // 获取两次刷新的时间间隔 mixer.update(delta); // 用时间间隔更新动画进度 renderer.render(scene, camera); }
如果你的模型有预设动画(比如游戏角色、机械臂),用 AnimationMixer 能很方便地控制这些复杂动作,不用自己写代码一点点定义。
Tween.js(属性过渡工具)
让物体的属性(位置、大小、颜色等)在一段时间内平滑变化(比如物体从 A 点慢慢移到 B 点,颜色从红慢慢变蓝)。
就像给物体设置 "自动轨迹",比如让一个方块 "在 2 秒内从左边滑到右边"。Tween.js 会自动计算中间的每一步位置,让移动看起来不生硬,而是平滑过渡。
- 补间(Tween):定义一个属性从 "起始值" 到 "目标值" 的变化过程,包括持续时间、变化速度(匀速、加速等)。
javascript// 假设已经创建了一个立方体cube // 创建补间:让立方体在2秒内从(0,0,0)移动到(5,0,0) const tween = new TWEEN.Tween(cube.position) // 要变化的属性(位置) .to({ x: 5 }, 2000) // 目标值(x=5)和持续时间(2000毫秒=2秒) .easing(TWEEN.Easing.Quadratic.InOut) // 变化方式(先慢后快再慢) .start(); // 开始执行补间 // 在动画循环中更新补间(让过渡生效) function animate() { requestAnimationFrame(animate); TWEEN.update(); // 刷新补间状态 renderer.render(scene, camera); }
手动写代码实现平滑过渡很麻烦(需要计算每帧的位置),而 Tween.js 能自动处理这些细节,让动画更自然。
2.3 光照与阴影:添加平行光 / 点光,开启阴影(castShadow/receiveShadow
)
在 3D 世界里,光照和阴影是让场景变得真实的关键 ------ 就像现实中阳光会照亮物体、地面会出现影子一样。Three.js 里的光照和阴影系统,其实就是在模拟这个自然现象。
光照:让物体 "看得见"
没有光的 3D 场景是全黑的,就像在漆黑的房间里什么都看不见。
平行光(DirectionalLight)
就像太阳光,光线从很远的地方照过来,所有光线都是平行的。
比如中午的太阳,不管照到近处的桌子还是远处的房子,光线方向都是一样的。
特点:
有明确的照射方向(比如从左上角照向右下角)
光照强度均匀,不会因为物体离得远就变暗
用法:
javascript// 创建平行光:参数1是光的颜色(0xffffff是白色),参数2是光照强度(0-1之间,1是最强) const directionalLight = new THREE.DirectionalLight(0xffffff, 1); // 设置光源的位置(相当于太阳在天空中的位置) directionalLight.position.set(10, 20, 15); // x=10, y=20, z=15 // 把光添加到场景中 scene.add(directionalLight);
点光(PointLight)
就像灯泡,光线从一个点向四面八方发散。
比如房间里的吊灯,会照亮周围所有方向,离灯泡越近的物体越亮,越远越暗。
特点:
有一个中心点,光线向四周扩散
光照强度会随距离衰减(远的地方暗)
用法:
javascript// 创建点光:参数1是颜色,参数2是强度,参数3是光照最大距离(超过这个距离就照不到了) const pointLight = new THREE.PointLight(0xffffff, 1, 100); // 设置点光的位置(相当于灯泡挂在哪个位置) pointLight.position.set(5, 5, 5); // 场景中间偏上的位置 // 把点光添加到场景中 scene.add(pointLight);
阴影:让物体 "有层次"
有了光,物体就会产生影子 ------ 这是让 3D 场景有立体感的关键。但 Three.js 里的阴影不是自动生成的,需要手动开启,就像 "告诉程序:请计算影子"。
核心概念:两个属性控制阴影
castShadow :"是否产生影子"
比如一个球,开启这个属性后,它被光照到时就会投下影子。
receiveShadow :"是否接收影子"
比如地面,开启这个属性后,其他物体的影子才能显示在它上面。
开启阴影的完整步骤(以平行光为例):
javascript
// 1. 告诉渲染器:需要计算阴影
renderer.shadowMap.enabled = true;
// 2. 告诉光源:需要产生阴影(不是所有光源都能产生阴影,平行光和点光可以)
directionalLight.castShadow = true;
// 3. 告诉物体:需要产生影子(比如一个立方体)
const cube = new THREE.Mesh(geometry, material);
cube.castShadow = true; // 立方体产生影子
// 4. 告诉地面:需要接收影子(比如一个平面当地面)
const groundGeometry = new THREE.PlaneGeometry(50, 50); // 大平面当地面
const groundMaterial = new THREE.MeshStandardMaterial({color: 0xcccccc});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.receiveShadow = true; // 地面接收影子
scene.add(ground);
2.4 交互:射线检测(Raycaster
)实现 "点击选中模型"
在 Three.js 中,要实现 "点击屏幕选中 3D 模型" 的功能,核心就是用射线检测(Raycaster)。这个功能就像我们用激光笔在现实中 "指" 东西一样 ------ 你在屏幕上点一下,Three.js 会发射一道 "虚拟激光",看看这道激光打到了哪个 3D 模型上。
为什么需要射线检测?
我们的屏幕是 2D 的(平面),而 Three.js 场景是 3D 的(立体)。当你在屏幕上点击时,计算机不知道你想选的是 3D 空间里的哪个物体。射线检测就是解决这个 "2D 点击对应 3D 物体" 的问题。
射线检测(Raycaster)的工作原理
1. 创建射线检测器(Raycaster)
就像准备好你的 "激光笔":
javascriptconst raycaster = new THREE.Raycaster(); // 射线检测器(激光笔)
2. 监听鼠标点击事件
告诉程序:"当用户点击屏幕时,触发检测":
javascript// 给网页添加鼠标点击事件 window.addEventListener('click', onMouseClick);
3. 计算射线的方向(关键步骤)
当用户点击时,需要把 2D 的屏幕坐标转换成 3D 射线的方向。
简单说就是:"用户点了屏幕上的 (x,y),这对应 3D 空间里哪条直线?"
javascriptfunction onMouseClick(event) { // 1. 获取鼠标在屏幕上的位置(归一化到-1到1之间) // 就像把屏幕坐标转换成"相对位置",方便Three.js计算 const mouse = new THREE.Vector2(); mouse.x = (event.clientX / window.innerWidth) * 2 - 1; // 横向坐标转换 mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; // 纵向坐标转换(注意Y轴方向相反) // 2. 让射线从相机位置出发,指向鼠标点击的方向 raycaster.setFromCamera(mouse, camera); }
4. 检测射线撞到了哪些模型
用射线检测场景中所有可能被选中的模型,然后处理选中的结果:
javascriptfunction onMouseClick(event) { // 前面的步骤:获取鼠标位置、设置射线方向...(同上) // 3. 准备一个数组,包含所有可能被点击的模型(比如场景中所有的网格) const allObjects = [cube, sphere, cylinder]; // 假设这些是你场景中的模型 // 4. 发射射线,检测哪些模型被射线击中 const intersects = raycaster.intersectObjects(allObjects); // 5. 如果有击中的模型,做一些操作(比如变色、放大等) if (intersects.length > 0) { // intersects[0]是距离相机最近的被击中的模型 const selectedObject = intersects[0].object; // 比如:把选中的模型变成红色 selectedObject.material.color.set(0xff0000); console.log('你选中了:', selectedObject); } }
2.5 实践
2.5.1 加载一个 "带动画的人物模型",实现点击模型播放动画
技术实现:
使用 GLTFLoader 加载包含动画数据的 glTF 格式模型
通过 AnimationMixer 控制动画播放
使用 Raycaster 实现模型点击检测
结合 OrbitControls 实现视角控制
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js 带动画的人物模型</title>
<!-- 引入Three.js -->
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<!-- 引入轨道控制器 -->
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/js/controls/OrbitControls.js"></script>
<!-- 引入GLTF加载器 -->
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/js/loaders/GLTFLoader.js"></script>
<!-- 引入Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 引入Font Awesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.backdrop-blur-xs {
backdrop-filter: blur(4px);
}
}
</style>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col">
<!-- 页面标题 -->
<header class="bg-indigo-600 text-white py-4 shadow-md">
<div class="container mx-auto px-4 flex justify-between items-center">
<h1 class="text-2xl font-bold">
<i class="fa fa-film mr-2"></i>带动画的3D人物模型
</h1>
<div class="text-sm bg-white/20 px-3 py-1 rounded-full">
点击模型播放/暂停动画
</div>
</div>
</header>
<!-- 主内容区 -->
<main class="flex-1 flex flex-col">
<!-- 3D渲染区域 -->
<div class="relative flex-1 w-full" id="canvas-container">
<!-- 加载指示器 -->
<div id="loading" class="absolute inset-0 flex items-center justify-center bg-white/80 z-10">
<div class="flex flex-col items-center">
<div class="w-16 h-16 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
<p class="mt-4 text-indigo-600 font-medium">加载模型中...</p>
</div>
</div>
<!-- 3D场景画布 -->
<canvas id="character-canvas" class="w-full h-full"></canvas>
<!-- 信息提示 -->
<div id="info" class="absolute bottom-4 left-4 bg-black/60 backdrop-blur-xs text-white px-4 py-2 rounded-lg text-sm opacity-0 transition-opacity duration-500">
<p>模型名称: <span id="model-name">-</span></p>
<p>当前动画: <span id="current-animation">-</span></p>
</div>
</div>
<!-- 动画控制区 -->
<div class="bg-white border-t border-gray-200 py-4 px-6">
<div class="container mx-auto">
<h2 class="text-lg font-semibold text-gray-800 mb-3">动画选择</h2>
<div class="flex flex-wrap gap-2" id="animation-controls">
<!-- 动画按钮将通过JS动态生成 -->
</div>
</div>
</div>
</main>
<!-- 页脚 -->
<footer class="bg-gray-800 text-gray-300 py-4">
<div class="container mx-auto px-4 text-center text-sm">
<p>使用 Three.js 实现的带动画人物模型演示</p>
</div>
</footer>
<script>
// 全局变量
let scene, camera, renderer, controls;
let characterModel = null;
let mixer = null;
let currentAction = null;
let animations = [];
let clock = new THREE.Clock();
let raycaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();
let isModelPlaying = false;
// DOM元素
const canvasContainer = document.getElementById('canvas-container');
const canvas = document.getElementById('character-canvas');
const loadingIndicator = document.getElementById('loading');
const infoPanel = document.getElementById('info');
const modelNameEl = document.getElementById('model-name');
const currentAnimationEl = document.getElementById('current-animation');
const animationControls = document.getElementById('animation-controls');
// 初始化Three.js场景
function initScene() {
// 创建场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
// 添加环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
// 添加方向光
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 7.5);
scene.add(directionalLight);
// 添加辅助网格
const gridHelper = new THREE.GridHelper(10, 10, 0xe0e0e0, 0xf0f0f0);
scene.add(gridHelper);
// 创建相机
camera = new THREE.PerspectiveCamera(
75,
canvasContainer.clientWidth / canvasContainer.clientHeight,
0.1,
1000
);
camera.position.z = 5;
camera.position.y = 1;
// 创建渲染器
renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true
});
renderer.setSize(canvasContainer.clientWidth, canvasContainer.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 创建轨道控制器
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.target.set(0, 1, 0); // 聚焦到人物腰部位置
// 窗口大小变化事件
window.addEventListener('resize', onWindowResize);
// 鼠标点击事件
window.addEventListener('click', onMouseClick);
}
// 加载带动画的人物模型
function loadAnimatedModel() {
// 使用GLTF加载器
const loader = new THREE.GLTFLoader();
// 加载示例人物模型(Three.js官方示例模型)
loader.load(
// 模型URL(包含动画的GLB模型)
'https://threejs.org/examples/models/gltf/RobotExpressive/RobotExpressive.glb',
// 加载成功回调
(gltf) => {
// 保存模型引用
characterModel = gltf.scene;
// 调整模型位置和缩放
characterModel.position.y = 0;
characterModel.scale.set(0.8, 0.8, 0.8);
// 添加到场景
scene.add(characterModel);
// 初始化动画混合器
mixer = new THREE.AnimationMixer(characterModel);
// 保存所有动画
animations = gltf.animations;
// 显示模型名称
modelNameEl.textContent = '机器人模型';
// 创建动画控制按钮
createAnimationButtons();
// 默认播放第一个动画
if (animations.length > 0) {
playAnimation(0);
}
// 隐藏加载指示器
loadingIndicator.classList.add('opacity-0');
setTimeout(() => {
loadingIndicator.classList.add('hidden');
infoPanel.classList.add('opacity-100');
}, 500);
},
// 加载进度回调
(xhr) => {
console.log(`加载进度: ${(xhr.loaded / xhr.total * 100).toFixed(0)}%`);
},
// 加载错误回调
(error) => {
console.error('模型加载错误:', error);
loadingIndicator.innerHTML = `
<div class="text-center">
<i class="fa fa-exclamation-triangle text-red-500 text-4xl mb-2"></i>
<p class="text-red-600">模型加载失败</p>
</div>
`;
}
);
}
// 创建动画控制按钮
function createAnimationButtons() {
// 清空现有按钮
animationControls.innerHTML = '';
// 为每个动画创建按钮
animations.forEach((animation, index) => {
// 简化动画名称(去除前缀和编号)
let animationName = animation.name;
animationName = animationName.replace(/^Take\d+_/, '');
animationName = animationName.charAt(0).toUpperCase() + animationName.slice(1);
const button = document.createElement('button');
button.className = `px-4 py-2 rounded-md text-sm font-medium transition-all ${
index === 0 ? 'bg-indigo-600 text-white' : 'bg-gray-200 text-gray-800 hover:bg-gray-300'
}`;
button.textContent = animationName;
button.dataset.index = index;
button.addEventListener('click', () => {
playAnimation(index);
// 更新按钮样式
document.querySelectorAll('#animation-controls button').forEach(btn => {
btn.classList.remove('bg-indigo-600', 'text-white', 'bg-gray-200', 'text-gray-800');
btn.classList.add('bg-gray-200', 'text-gray-800', 'hover:bg-gray-300');
});
button.classList.remove('bg-gray-200', 'text-gray-800', 'hover:bg-gray-300');
button.classList.add('bg-indigo-600', 'text-white');
});
animationControls.appendChild(button);
});
}
// 播放指定索引的动画
function playAnimation(index) {
if (!mixer || !animations[index]) return;
// 停止当前动画
if (currentAction) {
currentAction.fadeOut(0.3);
}
// 播放新动画
const animation = animations[index];
currentAction = mixer.clipAction(animation);
currentAction.reset();
currentAction.fadeIn(0.3);
currentAction.play();
// 更新信息面板
let animationName = animation.name.replace(/^Take\d+_/, '');
animationName = animationName.charAt(0).toUpperCase() + animationName.slice(1);
currentAnimationEl.textContent = animationName;
// 更新播放状态
isModelPlaying = true;
}
// 切换动画播放/暂停
function toggleAnimationPlay() {
if (!currentAction) return;
if (isModelPlaying) {
currentAction.pause();
} else {
currentAction.play();
}
isModelPlaying = !isModelPlaying;
}
// 窗口大小变化处理
function onWindowResize() {
const width = canvasContainer.clientWidth;
const height = canvasContainer.clientHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
}
// 鼠标点击事件处理
function onMouseClick(event) {
// 计算鼠标在标准化设备坐标中的位置 (-1 到 1)
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// 更新射线投射器
raycaster.setFromCamera(mouse, camera);
// 检查射线是否与模型相交
if (characterModel) {
const intersects = raycaster.intersectObject(characterModel, true);
// 如果点击了模型,切换动画播放状态
if (intersects.length > 0) {
toggleAnimationPlay();
// 添加点击反馈效果
const clickEffect = document.createElement('div');
clickEffect.className = 'fixed w-6 h-6 rounded-full bg-indigo-500/30 transform -translate-x-1/2 -translate-y-1/2 pointer-events-none';
clickEffect.style.left = `${event.clientX}px`;
clickEffect.style.top = `${event.clientY}px`;
clickEffect.style.animation = 'clickEffect 0.6s ease-out forwards';
document.body.appendChild(clickEffect);
// 添加动画样式
const style = document.createElement('style');
style.textContent = `
@keyframes clickEffect {
0% { transform: translate(-50%, -50%) scale(0); opacity: 1; }
100% { transform: translate(-50%, -50%) scale(2); opacity: 0; }
}
`;
document.head.appendChild(style);
// 移除效果元素
setTimeout(() => {
clickEffect.remove();
style.remove();
}, 600);
}
}
}
// 动画循环
function animate() {
requestAnimationFrame(animate);
// 更新动画混合器
if (mixer && isModelPlaying) {
mixer.update(clock.getDelta());
}
// 更新控制器
controls.update();
// 渲染场景
renderer.render(scene, camera);
}
// 初始化应用
function initApp() {
initScene();
loadAnimatedModel();
animate();
}
// 页面加载完成后初始化
window.addEventListener('load', initApp);
</script>
</body>
</html>
2.5.2 搭建一个 "3D 产品展厅",支持视角旋转和模型切换。
核心实现思路
基础三要素 :通过 Three.js 创建
场景(Scene)
、相机(Camera)
、渲染器(Renderer)
,构成 3D 展示的基础框架。视角控制 :使用
OrbitControls
实现鼠标拖拽旋转、滚轮缩放、右键平移,同时支持 "自动旋转" 开关。模型加载 :通过
GLTFLoader
加载通用 3D 模型(GLB/GLTF 格式),并提供多产品切换功能。轻量 UI:仅保留必要的控制按钮(旋转开关、模型切换、视角重置),避免冗余交互。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>极简3D产品展厅</title>
<style>
/* 基础样式:让Canvas占满屏幕,控制栏固定底部 */
body { margin: 0; overflow: hidden; font-family: sans-serif; }
#canvas { width: 100vw; height: 100vh; }
.controls {
position: fixed; bottom: 20px; left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.7); color: white;
padding: 10px 20px; border-radius: 8px;
display: flex; gap: 15px; align-items: center;
}
button {
padding: 6px 12px; border: none; border-radius: 4px;
background: #165DFF; color: white; cursor: pointer;
}
button:hover { background: #0E42D2; }
#model-select { padding: 6px; border-radius: 4px; }
</style>
<!-- 引入Three.js核心库和必要插件 -->
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/js/loaders/GLTFLoader.js"></script>
</head>
<body>
<!-- 3D渲染画布 -->
<canvas id="canvas"></canvas>
<!-- 控制栏:旋转开关、模型切换、视角重置 -->
<div class="controls">
<button id="rotate-btn">关闭自动旋转</button>
<select id="model-select">
<option value="1">产品1:简约台灯</option>
<option value="2">产品2:无线耳机</option>
<option value="3">产品3:智能手表</option>
</select>
<button id="reset-btn">重置视角</button>
</div>
<script>
// --------------------------
// 1. 初始化Three.js核心组件
// --------------------------
let scene, camera, renderer, controls;
let currentModel = null; // 存储当前加载的3D模型
// 场景:3D物体的"容器"
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0); // 浅灰色背景
// 相机:相当于"人眼",决定能看到什么
camera = new THREE.PerspectiveCamera(
75, // 视野角度(FOV)
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近裁剪面(太近的物体不显示)
1000 // 远裁剪面(太远的物体不显示)
);
camera.position.z = 5; // 相机初始位置(z轴远离原点)
// 渲染器:将场景和相机"画"到Canvas上
renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas') });
renderer.setSize(window.innerWidth, window.innerHeight); // 适配窗口大小
// 视角控制器:实现鼠标交互(旋转、缩放、平移)
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 平滑阻尼效果
controls.autoRotate = true; // 默认开启自动旋转
controls.autoRotateSpeed = 1; // 自动旋转速度(值越大越快)
// --------------------------
// 2. 添加场景辅助元素(光和网格)
// --------------------------
// 环境光:均匀照亮整个场景,避免物体过暗
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
// 方向光:模拟"太阳光",产生阴影和明暗对比
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 7.5); // 光源位置
scene.add(directionalLight);
// 网格辅助线:帮助判断物体位置(可选,便于调试)
const gridHelper = new THREE.GridHelper(20, 20, 0xcccccc, 0xcccccc);
scene.add(gridHelper);
// --------------------------
// 3. 模型加载与切换功能
// --------------------------
// 产品模型数据:存储不同产品的模型地址(这里用Three.js官方示例模型)
const productModels = {
1: "https://threejs.org/examples/models/gltf/LittlestTokyo.glb", // 台灯类模型
2: "https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf", // 耳机/头盔类模型
3: "https://threejs.org/examples/models/gltf/Nefertiti/Nefertiti.gltf" // 手表/小物件类模型
};
// 加载模型的函数
function loadModel(modelUrl) {
// 移除当前模型(避免多个模型叠加)
if (currentModel) {
scene.remove(currentModel);
}
// GLTF加载器:加载3D模型文件
const loader = new THREE.GLTFLoader();
loader.load(
modelUrl,
(gltf) => { // 加载成功回调
currentModel = gltf.scene; // 保存当前模型
// 调整模型大小(避免模型过大/过小)
const box = new THREE.Box3().setFromObject(currentModel);
const size = box.getSize(new THREE.Vector3()).length();
const scale = 3 / size; // 统一缩放到合适大小
currentModel.scale.set(scale, scale, scale);
// 居中模型(让模型在场景中心显示)
const center = box.getCenter(new THREE.Vector3());
currentModel.position.sub(center);
scene.add(currentModel); // 将模型添加到场景
},
(xhr) => { // 加载进度回调(可选)
console.log(`模型加载中:${Math.round(xhr.loaded / xhr.total * 100)}%`);
},
(error) => { // 加载失败回调(可选)
console.error("模型加载失败:", error);
}
);
}
// 初始加载第一个产品模型
loadModel(productModels[1]);
// --------------------------
// 4. 绑定UI控制事件
// --------------------------
// 1. 自动旋转开关
const rotateBtn = document.getElementById('rotate-btn');
rotateBtn.addEventListener('click', () => {
controls.autoRotate = !controls.autoRotate;
rotateBtn.textContent = controls.autoRotate ? "关闭自动旋转" : "开启自动旋转";
});
// 2. 模型切换(下拉选择框)
const modelSelect = document.getElementById('model-select');
modelSelect.addEventListener('change', (e) => {
const selectedProductId = e.target.value;
loadModel(productModels[selectedProductId]);
});
// 3. 重置视角
const resetBtn = document.getElementById('reset-btn');
resetBtn.addEventListener('click', () => {
camera.position.set(0, 0, 5); // 重置相机位置
controls.reset(); // 重置控制器状态
});
// --------------------------
// 5. 窗口 resize 适配(避免窗口缩放后画面变形)
// --------------------------
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix(); // 更新相机投影矩阵
renderer.setSize(window.innerWidth, window.innerHeight); // 更新渲染器大小
});
// --------------------------
// 6. 动画循环(让3D场景"动"起来)
// --------------------------
function animate() {
requestAnimationFrame(animate); // 循环调用自身
controls.update(); // 更新控制器状态(确保自动旋转和阻尼生效)
renderer.render(scene, camera); // 渲染场景
}
animate(); // 启动动画循环
</script>
</body>
</html>
3.Three.js 进阶
3.1 自定义着色器:用ShaderMaterial
写自定义顶点 / 片元着色器(如实现水波纹效果)
什么是着色器?
着色器 (Shader) 是一种专门处理图形渲染的小程序,运行在 GPU 上。就像画家画画有步骤一样,计算机渲染 3D 图形也需要特定步骤,着色器就是负责这些步骤的。
顶点着色器:处理物体的 "骨架",决定每个顶点的位置
片元着色器:处理物体的 "皮肤",决定每个像素的颜色
什么是 ShaderMaterial?
在 Three.js 中,Material (材质) 决定了物体的外观。而 ShaderMaterial 是一种特殊的材质,允许你编写自己的顶点着色器和片元着色器,实现各种炫酷效果.
普通材质 (如 MeshBasicMaterial) 的着色器是 Three.js 内置的,而 ShaderMaterial 让你可以 "自定义配方"。
水波纹效果的原理
水波纹效果本质上是让物体表面的顶点按照一定规律运动,再配合颜色变化,模拟水波扩散的效果:
顶点随时间上下起伏
距离波源越远,波动越小
颜色可能随波动高度变化(比如波峰更亮)
假设我们有一个平面,想让它看起来像水面:
创建一个平面几何体作为 "水面"
使用 ShaderMaterial,编写自定义着色器
在顶点着色器中:
根据时间和顶点位置计算波动高度
让顶点按照这个高度上下移动
在片元着色器中:
根据顶点的波动情况计算颜色
可能添加一些高光效果模拟水面反光
javascript// 创建自定义材质 const waterMaterial = new THREE.ShaderMaterial({ // 顶点着色器代码 vertexShader: ` // 接收从JavaScript传来的数据 uniform float time; attribute vec3 position; void main() { // 复制原始位置 vec3 newPosition = position; // 计算波动:使用正弦函数模拟波浪 // 随时间变化,x和z方向都有波动 newPosition.y = sin(time + position.x) * 0.5 + cos(time + position.z) * 0.3; // 计算最终位置 gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); } `, // 片元着色器代码 fragmentShader: ` uniform float time; void main() { // 基础蓝色 vec3 color = vec3(0.2, 0.5, 0.8); // 让颜色随时间轻微变化,增加动感 color += sin(time) * 0.05; // 设置像素颜色 gl_FragColor = vec4(color, 0.8); // 最后一个值是透明度 } `, // 告诉Three.js我们的材质需要透明 transparent: true }); // 每一帧更新时间,让波浪动起来 function animate() { requestAnimationFrame(animate); waterMaterial.uniforms.time.value += 0.01; renderer.render(scene, camera); }
=
3.2 性能优化:模型简化(BufferGeometry
)、纹理压缩、LOD(细节层次)
本质都是为了在保证视觉效果的同时,减少设备(手机、电脑)的运算压力,避免画面卡顿.
模型简化(BufferGeometry):给 3D 模型 "减肥",只留有用的 "骨架"
先想一个问题:我们看到的 3D 模型(比如游戏里的角色、场景中的桌子),其实不是 "实心" 的,而是由无数个 "小三角形"(叫 "面片")拼出来的 ------ 就像用乐高积木搭造型,积木越多,造型越精细,但拼起来越费时间。
模型简化的核心,就是减少这些 "小三角形" 的数量,同时尽量不破坏模型的整体样子;而 "BufferGeometry" 是实现简化的 "高效工具"(比如 Three.js、Unity 等 3D 软件里的核心技术)。
1. 为什么需要简化?(痛点)
比如你做了一个 "3D 苹果模型",为了追求真实,用了 10 万个小三角形 ------ 但当这个苹果在游戏里只是 "背景道具"(离玩家很远,看不清细节)时,10 万个三角形会让手机 / 电脑反复计算 "每个三角形的位置、颜色",导致画面卡顿。
这就像:你背书包上学,本来装 1 本课本就够了,却硬塞 10 本一模一样的,既累又没必要。
2. BufferGeometry 怎么 "减负"?(原理)
普通的 3D 模型数据(比如每个三角形的位置、颜色),会像 "散装快递" 一样杂乱存储,设备读取时要反复 "找数据",效率低;而 BufferGeometry 相当于把这些数据 "打包成整箱",按固定顺序排列。
举个类比:
普通方式:要找 "三角形 1 的位置、三角形 2 的颜色、三角形 1 的颜色、三角形 2 的位置"------ 东找西找,浪费时间;
BufferGeometry:先把 "所有三角形的位置" 放一起,再把 "所有三角形的颜色" 放一起 ------ 设备按顺序读 "整箱位置""整箱颜色",速度快 10 倍以上。
3. 简化后效果?(好处)
手机 / 电脑运算压力变小,画面更流畅;
模型文件体积变小(比如从 10MB 缩到 2MB),加载速度更快(不会出现 "卡半天加载不出场景" 的情况)。
纹理压缩:给 3D 模型的 "皮肤""压小",不占内存
如果说 "模型简化" 是减 "骨架",那 "纹理压缩" 就是减 "皮肤"------3D 模型表面的图案(比如角色的衣服纹理、墙面的砖块图案)叫 "纹理",本质是一张图片(比如 1024×1024 像素的图片)。
纹理压缩的核心:把纹理图片 "压缩变小",但视觉上几乎看不出模糊,同时让设备能快速读取。
1. 为什么需要压缩?(痛点)
一张 1024×1024 的 "未压缩纹理图",体积可能有 4MB(按 RGB 格式算:1024×1024×3 字节≈3MB,加上透明通道就是 4MB);如果一个场景里有 100 个这样的纹理,总大小就是 400MB------ 手机内存根本扛不住,还会导致 "纹理加载慢,画面出现'白块'"。
这就像:你手机里存 100 张 4MB 的照片,占 400MB 内存;如果把照片压缩成 1MB(清晰度没明显变化),100 张只占 100MB,省出的内存能装更多东西。
2. 怎么压缩?(原理,不用懂技术,看类比)
普通图片压缩(比如把 JPG 从 10MB 压到 2MB)会损失细节,但 "纹理压缩" 用了专门的 "硬件友好格式"(比如 ETC2、ASTC)------ 相当于给图片做 "智能压缩":
比如把 "一片红色区域" 的像素,只存 "红色 + 区域范围",而不是每个像素都存一次红色;
压缩后的图片,手机 GPU(负责画面运算的硬件)能直接 "解码使用",不用额外花时间处理。
3. 压缩后效果?(好处)
纹理文件体积缩小 50%-80%,加载快、不占内存;
视觉上几乎没区别(除非凑近看极端细节),不影响画面美观。
LOD(细节层次):让模型 "远近有别",聪明省资源
核心逻辑是:模型离你越近,用 "高细节版本"(三角形多、纹理清晰);离你越远,用 "低细节版本"(三角形少、纹理模糊) ,因为远处根本看不清细节,没必要浪费资源。
1. 为什么需要 LOD?(痛点)
如果没有 LOD,游戏里所有模型都用 "最高细节版本"------ 比如远处 100 米外的一棵树,明明看起来只是个 "绿点",却用了 1 万个三角形、4MB 的纹理,设备要花和 "近处角色" 一样的力气去计算,直接导致卡顿。
2. LOD 怎么工作?(原理)
开发者会给同一个模型做 "多个版本",比如一个 "树模型" 做 3 个版本:
LOD0(最高细节):10000 个三角形,2048×2048 纹理(离玩家<10 米时用,能看清树叶脉络);
LOD1(中等细节):2000 个三角形,1024×1024 纹理(离玩家 10-50 米时用,能看清树的形状,看不清脉络);
LOD2(最低细节):500 个三角形,512×512 纹理(离玩家>50 米时用,只看出是 "绿色的树轮廓")。
游戏运行时,会自动判断 "玩家和树的距离",切换对应的版本 ------ 近用 LOD0,远用 LOD2,既不影响视觉,又省资源。
3. LOD 的好处?
设备只在 "需要精细画面" 时用高资源,远处自动降资源,整体运算压力大减;
画面流畅度提升,同时远处场景不会因为 "细节太低" 变模糊(因为远处本就看不清)
总结
知识点 | 类比对象 | 核心作用 | 解决的问题 |
---|---|---|---|
模型简化 | 给骨架减肥 | 减少三角形数量,优化数据存储 | 模型运算慢、文件大 |
纹理压缩 | 给皮肤 "瘦身" | 缩小纹理图片,快速加载 | 纹理占内存、加载出白块 |
LOD(细节层次) | 远近穿不同衣服 | 近用高细节,远用低细节 | 远处模型浪费资源导致卡顿 |
3.3 后期处理:EffectComposer
实现模糊、泛光效果
什么是后期处理?
简单说,后期处理就像照片的 "滤镜",是在 3D 场景渲染完成后,对最终画面添加的特效处理。比如让画面变模糊、加光晕、调颜色等,能让场景看起来更有质感。
EffectComposer 是什么?
EffectComposer
是 Three.js 的一个工具(需要额外引入扩展库),专门用来管理和实现各种后期处理效果。它的工作流程类似:
先正常渲染 3D 场景
把渲染结果交给各种特效 "处理器"
最后把处理好的画面显示到屏幕上
如何实现模糊效果?
模糊效果就像给画面打了一层柔光,让图像边缘变得不那么锐利。
实现步骤:
引入必要的库(Three.js 核心库 + 后期处理扩展库)
创建场景、相机、物体(基础 3D 场景)
初始化
EffectComposer
(后期处理组合器)添加特效通道(模糊、泛光等)
在动画循环中更新组合器,而不是直接渲染场景
javascript// 1. 创建场景、相机、渲染器 const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // 2. 添加一个物体(比如一个立方体) const geometry = new THREE.BoxGeometry(); const material = new THREE.MeshBasicMaterial({color: 0x00ff00, wireframe: true}); const cube = new THREE.Mesh(geometry, material); scene.add(cube); camera.position.z = 5; // 3. 初始化后期处理组合器 const composer = new THREE.EffectComposer(renderer); // 4. 添加渲染通道(先把场景正常渲染到临时画布) const renderPass = new THREE.RenderPass(scene, camera); composer.addPass(renderPass); // 5. 添加模糊效果通道 const blurPass = new THREE.ShaderPass(THREE.GaussianBlurShader); blurPass.uniforms['sigma'].value = 5; // 模糊程度(值越大越模糊) composer.addPass(blurPass); // 6. 添加泛光效果通道 const bloomPass = new THREE.UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, // 泛光强度 0.4, // 泛光半径 0.85 // 泛光阈值(值越小,越多物体产生泛光) ); composer.addPass(bloomPass); // 7. 动画循环(用composer渲染,而不是renderer) function animate() { requestAnimationFrame(animate); cube.rotation.x += 0.01; cube.rotation.y += 0.01; // 注意这里不再用 renderer.render(scene, camera) composer.render(); // 用组合器渲染,自动应用所有特效 } animate();