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

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

总结


  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


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

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 中实例化这个工具

javascript 复制代码
const 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 里的阴影不是自动生成的,需要手动开启,就像 "告诉程序:请计算影子"。

核心概念:两个属性控制阴影
  1. castShadow :"是否产生影子"

    比如一个球,开启这个属性后,它被光照到时就会投下影子。

  2. 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)

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

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

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

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

当用户点击时,需要把 2D 的屏幕坐标转换成 3D 射线的方向。

简单说就是:"用户点了屏幕上的 (x,y),这对应 3D 空间里哪条直线?"

javascript 复制代码
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. 检测射线撞到了哪些模型

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

javascript 复制代码
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 实现视角控制

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

核心实现思路

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

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

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

  4. 轻量 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 让你可以 "自定义配方"。

水波纹效果的原理


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

  1. 顶点随时间上下起伏

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

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

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

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

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

  3. 在顶点着色器中:

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

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

  4. 在片元着色器中:

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

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

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

  1. 先正常渲染 3D 场景

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

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

如何实现模糊效果?


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

实现步骤:

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

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

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

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

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

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();
相关推荐
茯苓gao10 小时前
STM32G4 电流环闭环
笔记·stm32·单片机·嵌入式硬件·学习
easy202010 小时前
机器学习的本质:从跑模型到真正解决问题
笔记·学习·机器学习
普蓝机器人13 小时前
AutoTrack-IR-DR200仿真导航实验详解:为高校打造的机器人学习实践平台
人工智能·学习·机器人·移动机器人·三维仿真导航
非凡ghost14 小时前
AOMEI Partition Assistant磁盘分区工具:磁盘管理的得力助手
linux·运维·前端·数据库·学习·生活·软件需求
m0_5782678614 小时前
从零开始的python学习(九)P142+P143+P144+P145+P146
笔记·python·学习
非凡ghost14 小时前
简朴App(PlainApp):开源、隐私保护的手机管理工具
学习·智能手机·生活·软件需求
晨非辰14 小时前
#C语言——刷题攻略:牛客编程入门训练(十):攻克 循环控制(二),轻松拿捏!
c语言·开发语言·经验分享·学习·visual studio
有谁看见我的剑了?15 小时前
k8s-临时容器学习
学习·容器·kubernetes