5.Three.js 学习(基础+实践)

发布于:2025-09-14 ⋅ 阅读:(17) ⋅ 点赞:(0)

Three.js 是 “WebGL 的封装库”,帮你屏蔽了底层的着色器 / 缓冲区细节,专注于 “3D 场景搭建”,开发效率高,是通用 3D 开发的首选。

他的核心是 “场景 - 相机 - 渲染器” 的联动逻辑,先掌握基础组件,再学进阶功能(如自定义着色器)

实践只提供参考代码,自己去找模型尝试。

1.Three.js 核心组件

1.1 三要素:场景(Scene)、相机(PerspectiveCamera/OrthographicCamera)、渲染器(WebGLRenderer

场景(Scene


场景就像是一个虚拟的 "3D 舞台",所有的 3D 物体(比如模型、灯光、粒子等)都需要放在这个舞台上才能被看到。

场景本身不直接显示任何东西,它只是一个容器,负责管理所有需要渲染的对象。在 Three.js 中,你可以通过add()方法往场景里添加各种元素。

// 创建一个场景
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 度)

  • 宽高比:画面的宽度和高度比例(通常是浏览器窗口的宽高比)

  • 近平面:离相机多近的物体才会被看到

  • 远平面:离相机多远的物体还能被看到

// 创建透视相机
const camera = new THREE.PerspectiveCamera(
  75, // 视野角度
  window.innerWidth / window.innerHeight, // 宽高比
  0.1, // 近平面
  1000 // 远平面
);
camera.position.z = 5; // 把相机往后移动一点,这样能看到场景中的物体

正交相机(OrthographicCamera)

这种相机没有透视效果,远处和近处的物体看起来一样大,适合 2D 渲染或建筑图纸等需要精确尺寸的场景。

// 创建正交相机
const camera = new THREE.OrthographicCamera(
  window.innerWidth / -2, // 左边界
  window.innerWidth / 2,  // 右边界
  window.innerHeight / 2, // 上边界
  window.innerHeight / -2,// 下边界
  0.1, // 近平面
  1000 // 远平面
);

渲染器(WebGLRenderer)


渲染器的作用是把场景和相机 "结合" 起来,计算出最终应该显示在屏幕上的图像,并把它绘制到网页的 canvas 元素上。

// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染器的尺寸为窗口大小
document.body.appendChild(renderer.domElement); // 将渲染器生成的canvas元素添加到网页中

// 执行渲染(相当于"拍照")
renderer.render(scene, camera);

总结


  1. 场景是舞台,存放所有物体

  2. 相机是观众的眼睛,决定看什么角度

  3. 渲染器是画笔,把相机看到的场景画在屏幕上

当你想要制作动画时,只需要在一个循环中不断改变场景中物体的位置或相机角度,然后重新调用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


网格是几何体和材质的 "结合体",就像把皮肤贴在骨架上,形成一个可以显示在屏幕上的具体物体。

// 创建一个立方体骨架
const geometry = new THREE.BoxGeometry(1, 1, 1);
// 创建一个红色皮肤
const material = new THREE.MeshBasicMaterial({color: 0xff0000});
// 把皮肤贴在骨架上,得到一个红色立方体
const cube = new THREE.Mesh(geometry, material);
scene.add(cube); // 把物体放入场景

1.3 辅助工具:坐标轴(AxesHelper)、性能监控(Stats

在 Three.js 中,辅助工具就像我们做手工时用的尺子、放大镜一样,能帮我们更方便地调试和观察 3D 场景。

坐标轴(AxesHelper)


在 3D 场景中显示 X、Y、Z 三条坐标轴,帮你判断物体的位置和方向.

// 创建一个长度为5的坐标轴(数值越大,线越长)
const axesHelper = new THREE.AxesHelper(5);
// 把坐标轴放进场景,就能看到了
scene.add(axesHelper);

 性能监控(Stats)


实时显示 3D 场景的运行性能,比如每秒渲染多少帧(FPS).

  • FPS(Frames Per Second):每秒渲染的画面数量。数值越高,画面越流畅(通常 60 是比较理想的状态)。

  • 如果 FPS 低于 30,画面会感觉卡顿,就像看幻灯片

// 创建性能监控器
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 场景”:包含立方体、球体、地面,添加坐标轴和光照。

代码:

<!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 核心库里没有自带这个工具,需要额外引入

<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/GLTFLoader.js"></script>

创建 "搬运工"

在 JavaScript 中实例化这个工具

const loader = new THREE.GLTFLoader();

指定要搬运的模型文件

告诉模型文件在哪里(文件路径)

// 加载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):管理这些动画片段的播放器,能控制播放速度、切换动作、甚至混合多个动作(比如边走路边挥手)。

// 假设已经加载了一个带动画的模型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):定义一个属性从 "起始值" 到 "目标值" 的变化过程,包括持续时间、变化速度(匀速、加速等)。

// 假设已经创建了一个立方体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)

就像太阳光,光线从很远的地方照过来,所有光线都是平行的。
比如中午的太阳,不管照到近处的桌子还是远处的房子,光线方向都是一样的。

特点

  • 有明确的照射方向(比如从左上角照向右下角)

  • 光照强度均匀,不会因为物体离得远就变暗

用法:

// 创建平行光:参数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)

就像灯泡,光线从一个点向四面八方发散。
比如房间里的吊灯,会照亮周围所有方向,离灯泡越近的物体越亮,越远越暗。

特点:

  • 有一个中心点,光线向四周扩散

  • 光照强度会随距离衰减(远的地方暗)

用法:

// 创建点光:参数1是颜色,参数2是强度,参数3是光照最大距离(超过这个距离就照不到了)
const pointLight = new THREE.PointLight(0xffffff, 1, 100);
// 设置点光的位置(相当于灯泡挂在哪个位置)
pointLight.position.set(5, 5, 5); // 场景中间偏上的位置
// 把点光添加到场景中
scene.add(pointLight);

阴影:让物体 "有层次"


有了光,物体就会产生影子 —— 这是让 3D 场景有立体感的关键。但 Three.js 里的阴影不是自动生成的,需要手动开启,就像 "告诉程序:请计算影子"。

核心概念:两个属性控制阴影
  1. castShadow:"是否产生影子"
    比如一个球,开启这个属性后,它被光照到时就会投下影子。

  2. receiveShadow:"是否接收影子"
    比如地面,开启这个属性后,其他物体的影子才能显示在它上面。

开启阴影的完整步骤(以平行光为例):
// 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)

就像准备好你的 "激光笔":

 
const raycaster = new THREE.Raycaster(); // 射线检测器(激光笔)
2. 监听鼠标点击事件

告诉程序:"当用户点击屏幕时,触发检测":

// 给网页添加鼠标点击事件
window.addEventListener('click', onMouseClick);
3. 计算射线的方向(关键步骤)

当用户点击时,需要把 2D 的屏幕坐标转换成 3D 射线的方向。
简单说就是:"用户点了屏幕上的 (x,y),这对应 3D 空间里哪条直线?"

 
function 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. 检测射线撞到了哪些模型

用射线检测场景中所有可能被选中的模型,然后处理选中的结果:

 
function 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 实现视角控制

<!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 产品展厅”,支持视角旋转和模型切换。

核心实现思路

  1. 基础三要素:通过 Three.js 创建场景(Scene)相机(Camera)渲染器(Renderer),构成 3D 展示的基础框架。

  2. 视角控制:使用OrbitControls实现鼠标拖拽旋转、滚轮缩放、右键平移,同时支持 “自动旋转” 开关。

  3. 模型加载:通过GLTFLoader加载通用 3D 模型(GLB/GLTF 格式),并提供多产品切换功能。

  4. 轻量 UI:仅保留必要的控制按钮(旋转开关、模型切换、视角重置),避免冗余交互。

<!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 让你可以 "自定义配方"。

水波纹效果的原理


水波纹效果本质上是让物体表面的顶点按照一定规律运动,再配合颜色变化,模拟水波扩散的效果:

  1. 顶点随时间上下起伏

  2. 距离波源越远,波动越小

  3. 颜色可能随波动高度变化(比如波峰更亮)

假设我们有一个平面,想让它看起来像水面:

  1. 创建一个平面几何体作为 "水面"

  2. 使用 ShaderMaterial,编写自定义着色器

  3. 在顶点着色器中:

    • 根据时间和顶点位置计算波动高度

    • 让顶点按照这个高度上下移动

  4. 在片元着色器中:

    • 根据顶点的波动情况计算颜色

    • 可能添加一些高光效果模拟水面反光

// 创建自定义材质
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 的一个工具(需要额外引入扩展库),专门用来管理和实现各种后期处理效果。它的工作流程类似:

  1. 先正常渲染 3D 场景

  2. 把渲染结果交给各种特效 "处理器"

  3. 最后把处理好的画面显示到屏幕上

如何实现模糊效果?


模糊效果就像给画面打了一层柔光,让图像边缘变得不那么锐利。

实现步骤:

  1. 引入必要的库(Three.js 核心库 + 后期处理扩展库)

  2. 创建场景、相机、物体(基础 3D 场景)

  3. 初始化 EffectComposer(后期处理组合器)

  4. 添加特效通道(模糊、泛光等)

  5. 在动画循环中更新组合器,而不是直接渲染场景

// 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();