普通视图

发现新文章,点击刷新页面。
昨天以前首页

Three.js一起学-如何通过官方例子高效学习 Three.js?手把手带你“抄”出一个3D动画

2026年3月16日 18:16

前言

在之前的文章中有说过,我认为学习three.js最好的方法就是通过官网的例子去学习,在实践中练习。 因为Three.js中api很多,没有系统的学习文档,因此我觉得通过例子去学习,使用到某个概念或者api在深入学习它,这是一个不错的学习方式。 那么具体如何学习呢?

45ce1782386deb25cf01ea76ee0705b6.gif

好了言归正传,下面我就教大家如何打出一套如来神掌,啊不对,是通过官方例子学习Three.js。

0CFA5E8C.jpg

准备

阅读本文需要一点点的 WebGL 的知识点,至少这个文档的基础知识部分看完即可。

此外各位同学需要下载一下 Three.js 源码。源码里有我们需要用到的模型和官网例子的源码。

搭建一个 web 工程,本文演示创建基于 vue3 的工程。

bash

npm init vue@latest

按照自己的喜好选择要安装哪些插件即可。生成的项目结构如下,我们为此项目添加 three.js,修改 package.json 如下。

json

{
  "dependencies": {
    ...
    "three": "^0.143.0"
  }
}

如需使用 typescript,添加如下依赖:

json

{
  "devDependencies": {
    ...  
    "@types/three": "^0.143.0"
  }
}

然后执行 npm install 安装依赖即可。

至此我们前期创建项目的准备工作就做完了。

体验例子

接下来我们在 three.js 官网上找到 感兴趣的例子,打开它的源码,先大致阅读一遍。通过这个例子中我们将学到如下几个知识点:相机、场景、网格、灯光、材质、形状、动画等。下面我们跟着例子写一遍代码,带大家学会如何通过例子去学习。

j1j33-c8781.gif

写(抄)代码

源码在上面的链接已经给出来,我就不在这里凑字数了。我在这里一步一步地写一下上述例子的代码,并演示如何通过此例来学习 Three.js。那么就让我们愉快地开始吧。

首先,我们在 Vue 项目中创建一个组件(比如 ThreeExample.vue),并在 mounted 生命周期中编写 Three.js 代码。当然,你也可以用原生 HTML+JS,但这里我们以 Vue3 为例。

1. 初始化场景、相机和渲染器

打开官方例子的源码,我们会看到一开始就创建了 scenecamerarenderer 这三个基本对象。这是每一个 Three.js 应用的起点。

js

// 1. 创建场景
const scene = new THREE.Scene();

// 2. 创建透视相机(参数:视野角度、宽高比、近裁面、远裁面)
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000);
camera.position.set(2, 2, 5); // 设置相机位置
camera.lookAt(0, 0, 0);        // 让相机看向原点

// 3. 创建 WebGL 渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement); // 将 canvas 添加到页面

学习点:这里我们用到了 PerspectiveCamera,如果不清楚它的参数含义,就可以去 官网文档 查看。文档会详细解释每个参数的作用,比如 fov(视野角度)决定了你能看到多大的范围,aspect(宽高比)通常设为画布的宽度/高度,否则图像会被拉伸。

2. 添加光源

例子中使用了多种光源:环境光、点光源和聚光灯。光源是让物体可见并产生阴影和立体感的关键。

js

// 环境光:提供基础照明,均匀照亮所有面
const ambientLight = new THREE.AmbientLight(0xffffff, 0.2);
scene.add(ambientLight);

// 点光源:从某个点向所有方向发射光线
const pointLight = new THREE.PointLight(0xffffff, 1);
pointLight.position.set(1, 2, 3);
scene.add(pointLight);

// 还可以添加其他光源,比如聚光灯、平行光等,根据需要选择

学习点:看到 AmbientLight 和 PointLight,我们可以去文档了解每种光源的特点和适用场景。例如环境光没有方向,通常用来提亮阴影部分;点光源类似灯泡,会产生阴影(需配合阴影设置)。

3. 加载模型

例子中加载了一个带有骨骼动画和变形动画的模型(models/gltf/Soldier.glb)。我们需要使用 GLTFLoader 来加载 glTF 格式的模型。

首先,引入加载器(需要额外导入):

js

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

然后,在代码中创建加载器并加载模型:

js

const loader = new GLTFLoader();
loader.load('models/gltf/Soldier.glb', (gltf) => {
  const model = gltf.scene;
  scene.add(model);

  // 获取模型的动画剪辑
  const animations = gltf.animations;
  if (animations && animations.length) {
    // 创建动画混合器并播放动画(后面会讲到)
    mixer = new THREE.AnimationMixer(model);
    const action = mixer.clipAction(animations[0]);
    action.play();
  }
}, undefined, (error) => {
  console.error('模型加载失败:', error);
});

注意:模型路径需要根据你存放的位置调整。官方源码中的模型路径是相对 examples/ 目录的,你可以把 models 文件夹复制到你的 public 目录下。

学习点:遇到 GLTFLoader,我们可以去文档或源码中查看它的用法。GLTF 是 Three.js 推荐的 3D 模型格式,支持动画、材质、骨骼等。通过这个例子,我们学会了如何加载外部模型并添加到场景。

4. 动画循环

例子中使用 requestAnimationFrame 实现动画循环,并在每一帧更新动画混合器。

js

let mixer = null; // 在加载模型时赋值

function animate() {
  requestAnimationFrame(animate);

  const delta = clock.getDelta(); // 获取时间差,用于平滑动画
  if (mixer) {
    mixer.update(delta); // 更新动画混合器
  }

  // 渲染场景
  renderer.render(scene, camera);
}

// 创建 Clock 对象用于计算时间差
const clock = new THREE.Clock();

// 启动动画循环
animate();

学习点AnimationMixer 和 Clock 是 Three.js 中处理动画的重要 API。通过查看文档,我们可以了解 mixer.update(delta) 如何基于时间差驱动模型动画。

5. 处理窗口大小变化

为了让画布自适应窗口,我们需要监听窗口的 resize 事件,更新相机宽高比和渲染器尺寸。

js

window.addEventListener('resize', onWindowResize, false);

function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix(); // 必须调用,使相机参数生效
  renderer.setSize(window.innerWidth, window.innerHeight);
}

学习点updateProjectionMatrix() 方法的作用是更新相机的投影矩阵,当相机参数改变时(如宽高比),需要调用此方法让 Three.js 重新计算投影。

6. 遇到不懂的就去官网查找 API

在“抄”代码的过程中,你一定会遇到很多陌生的 API,比如 PointLightGLTFLoaderAnimationMixerClockupdateProjectionMatrix 等等。这时候,最好的学习方式就是打开 Three.js 官方文档,搜索这些 API,仔细阅读其用法和参数含义。

例如,你看到 camera.lookAt(0, 0, 0),可以查阅 Object3D 的 lookAt 方法,理解它是如何让物体朝向某个点的。

文档通常包含详细说明和示例代码,非常有助于深入理解。通过这样的方式,你不仅学会了这个例子,还能举一反三,应用到其他场景。

完整的代码整合

将上述代码片段整合到一个 Vue 组件中,大概如下(省略了样式和模板部分):

vue

<template>
  <div ref="container" style="width:100%; height:100vh;"></div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

const container = ref(null);

onMounted(() => {
  // 1. 创建场景
  const scene = new THREE.Scene();

  // 2. 创建相机
  const camera = new THREE.PerspectiveCamera(45, container.value.clientWidth / container.value.clientHeight, 1, 2000);
  camera.position.set(2, 2, 5);
  camera.lookAt(0, 0, 0);

  // 3. 创建渲染器
  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(container.value.clientWidth, container.value.clientHeight);
  renderer.setPixelRatio(window.devicePixelRatio);
  container.value.appendChild(renderer.domElement);

  // 4. 添加光源
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.2);
  scene.add(ambientLight);
  const pointLight = new THREE.PointLight(0xffffff, 1);
  pointLight.position.set(1, 2, 3);
  scene.add(pointLight);

  // 5. 加载模型
  const loader = new GLTFLoader();
  let mixer = null;
  loader.load('/models/gltf/Soldier.glb', (gltf) => {
    const model = gltf.scene;
    scene.add(model);
    if (gltf.animations.length) {
      mixer = new THREE.AnimationMixer(model);
      const action = mixer.clipAction(gltf.animations[0]);
      action.play();
    }
  });

  // 6. 动画循环
  const clock = new THREE.Clock();
  function animate() {
    requestAnimationFrame(animate);

    const delta = clock.getDelta();
    if (mixer) {
      mixer.update(delta);
    }

    renderer.render(scene, camera);
  }
  animate();

  // 7. 窗口大小自适应
  const onWindowResize = () => {
    camera.aspect = container.value.clientWidth / container.value.clientHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(container.value.clientWidth, container.value.clientHeight);
  };
  window.addEventListener('resize', onWindowResize);
});
</script>

如何进一步深入学习

通过“抄”这个例子,我们已经接触到了 Three.js 的核心概念。但这仅仅是开始。下面是一些进阶学习建议:

  1. 修改参数,观察效果:尝试调整相机位置、光源颜色和强度、模型缩放比例等,实时查看变化,加深理解。
  2. 尝试添加交互:比如使用 OrbitControls 让用户可以用鼠标旋转视角。引入对应的控制器并启用,能极大地提升体验。
  3. 研究动画:例子中只播放了第一个动画剪辑。你可以尝试播放其他动画,或者混合多个动画。
  4. 阅读更多例子:Three.js 官网有上百个例子,涵盖了粒子系统、后期处理、物理效果等。按照同样的方法,一个个“抄”过去,你的 Three.js 水平会飞速提升。
  5. 参与社区:遇到问题时,可以到 Stack Overflow、GitHub Issues 或中文社区(如掘金)提问,也可以阅读他人的源码和文章。

最后

以上就是本文的全部内容了。我们通过一个官方的骨骼动画例子,逐步学习了 Three.js 的基础:场景、相机、渲染器、光源、模型加载、动画和自适应窗口。更重要的是,我们实践了“通过例子学习”的方法——遇到不懂的 API 就去查文档,然后亲手写一遍。

这种学习方法不仅适用于 Three.js,也适用于其他任何技术栈。希望各位同学能举一反三,不再畏惧陌生的框架和库,勇敢地打开源码,开始你的“抄”级学习之旅!

如果感觉阅读本文后有所收获,欢迎点赞、收藏和评论,也欢迎分享你通过例子学习 Three.js 的心得。我们下期再见!

后续会持续更新我的WebGL和Three.js学习过程和经验分享,如果感兴趣可以关注我的专栏Three.js一起来学

相关文章会同步发布到我的公众号:【编程智匠】

【Three.js 与 Shader】编写你的第一个自定义着色器,让模型拥有灵魂

作者 叶智辽
2026年3月13日 08:46

前言

以前我觉得 Shader 是神仙才能看懂的东西,直到我发现它其实就是告诉 GPU“怎么画”的说明书。

两年前我第一次接触 Shader,看了一篇教程,开头就是 gl_FragColorvaryinguniform 这些天书一样的词。我心想:这玩意儿是人写的吗?

后来项目里有个需求:让模型边缘发蓝光。网上搜了一堆方案,最后发现除了自己写 Shader 别无他法。硬着头皮啃了一周,终于把第一个能跑起来的着色器怼出来了。运行起来的那一刻,模型边缘真的泛着蓝光,我当时激动得差点原地蹦起来。

原来 Shader 没那么可怕,它就像一本固定的菜谱,你告诉 GPU“颜色怎么混合、顶点怎么移动”,它就能画出你想要的效果。

今天我就用最笨的方式,带你写三个最简单的自定义着色器。不扯虚的,代码直接跑,效果直接看。


一、Shader 是啥?

通俗点说,渲染一个 3D 模型要经过两个阶段:

  • 顶点着色器(Vertex Shader):负责处理每个顶点的位置、变换。好比整容医生,决定骨架长啥样。
  • 片元着色器(Fragment Shader):负责计算每个像素的颜色。好比化妆师,给每个点涂上什么颜色。

Three.js 默认的材质(比如 MeshStandardMaterial)内部已经有一套写好的着色器。我们用自定义着色器,就是替换掉默认的,自己控制这两个阶段。


二、Three.js 里的自定义材质

Three.js 提供了两种方式来写自定义着色器:

  • ShaderMaterial:自动帮你补全一些默认的 uniform 和 attribute,适合初学者。
  • RawShaderMaterial:完全自己控制,什么都不帮你补,适合进阶。

我们先从 ShaderMaterial 开始,省事。

基础结构

const material = new THREE.ShaderMaterial({
  uniforms: {
    time: { value: 0 },
    color: { value: new THREE.Color(0xffaa00) }
  },
  vertexShader: `
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform vec3 color;
    void main() {
      gl_FragColor = vec4(color, 1.0);
    }
  `
});
  • uniforms:可以从 JavaScript 传给着色器的变量,每帧可以更新。
  • vertexShader:顶点着色器代码,字符串形式。
  • fragmentShader:片元着色器代码。

gl_Position 是顶点着色器必须输出的最终位置,gl_FragColor 是片元着色器必须输出的最终颜色。


三、第一个例子:让模型颜色随时间变化

我们用上面的基础结构,加一个 time uniform,让颜色在红色和蓝色之间循环。

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(2, 2, 5);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

new OrbitControls(camera, renderer.domElement);

// 创建一个立方体
const geometry = new THREE.BoxGeometry(2, 2, 2);

// 自定义着色器材质
const material = new THREE.ShaderMaterial({
  uniforms: {
    time: { value: 0 }
  },
  vertexShader: `
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform float time;
    void main() {
      // 让红色分量在 0.5 到 1.0 之间变化,蓝色分量反向变化
      float r = 0.6 + 0.4 * sin(time);
      float b = 0.6 + 0.4 * cos(time);
      gl_FragColor = vec4(r, 0.2, b, 1.0);
    }
  `
});

const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

function animate() {
  requestAnimationFrame(animate);

  // 更新 uniform 中的时间
  material.uniforms.time.value += 0.05;

  renderer.render(scene, camera);
}
animate();

把这段代码贴进一个 HTML 文件里运行,你会看到一个立方体颜色在紫色系里渐变。


四、第二个例子:让顶点动起来(波浪效果)

现在我们来动顶点。让立方体的顶点按照正弦波上下浮动,像一块果冻。

const geometry = new THREE.BoxGeometry(2, 2, 2, 32, 32, 32); // 增加细分段数,让波浪更平滑

const material = new THREE.ShaderMaterial({
  uniforms: {
    time: { value: 0 }
  },
  vertexShader: `
    uniform float time;
    void main() {
      // 根据顶点原来的位置计算偏移量
      float offsetX = sin(position.y * 2.0 + time * 3.0) * 0.2;
      float offsetZ = cos(position.y * 2.0 + time * 2.0) * 0.2;
      vec3 newPosition = position + vec3(offsetX, 0.0, offsetZ);
      
      gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
    }
  `,
  fragmentShader: `
    void main() {
      gl_FragColor = vec4(0.6, 0.8, 1.0, 1.0);
    }
  `
});

这里的关键是:我们在顶点着色器里修改了 position,然后再进行 MVP 变换。注意 position 是模型局部坐标,计算时要小心不要破坏模型结构。

运行后,立方体的侧面会像波浪一样起伏。


五、第三个例子:简单边缘光

边缘光(Fresnel Effect)是让模型边缘发光的常见效果。原理是视线方向与法线方向越垂直(边缘),光越强。

我们需要在顶点着色器里把法线和视线方向传给片元着色器。

const material = new THREE.ShaderMaterial({
  uniforms: {
    time: { value: 0 }
  },
  vertexShader: `
    varying vec3 vNormal;
    varying vec3 vViewPosition;
    
    void main() {
      vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
      vViewPosition = -mvPosition.xyz; // 指向相机的方向
      vNormal = normalize(normalMatrix * normal); // 将法线转换到视图空间
      
      gl_Position = projectionMatrix * mvPosition;
    }
  `,
  fragmentShader: `
    varying vec3 vNormal;
    varying vec3 vViewPosition;
    
    void main() {
      vec3 normal = normalize(vNormal);
      vec3 viewDir = normalize(vViewPosition);
      
      // 计算视线与法线的点积,越接近0(垂直)强度越大
      float fresnel = 1.0 - abs(dot(normal, viewDir));
      fresnel = pow(fresnel, 2.0); // 增强对比
      
      vec3 baseColor = vec3(0.3, 0.6, 1.0);
      vec3 edgeColor = vec3(0.8, 0.9, 1.0);
      
      vec3 finalColor = mix(baseColor, edgeColor, fresnel);
      
      gl_FragColor = vec4(finalColor, 1.0);
    }
  `
});

这个效果在球体上最明显,模型边缘会有一圈亮光,非常有科技感。


六、坑点总结

  1. 矩阵顺序projectionMatrix * modelViewMatrix * vec4(position, 1.0) 顺序不能错,Three.js 的矩阵是右乘,坐标从右向左变换。
  2. 法线变换:不能用 modelViewMatrix 直接乘法线,要用 normalMatrix(它是 modelViewMatrix 的逆转置的左上3x3)。
  3. 变量精度:移动设备上可能需要指定精度,比如 precision highp float; 放在着色器开头。
  4. uniform 更新:记得在动画循环里更新 material.uniforms.xxx.value
  5. 调试技巧:可以用 gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); 先确认片元着色器是否运行,用 gl_Position = vec4(position, 1.0);(跳过 MVP)看顶点原始位置。

七、进阶方向

这三个例子只是冰山一角。有了自定义着色器,你还可以做:

  • 流光效果
  • 溶解消失
  • 噪声纹理生成地形
  • 后处理滤镜

Three.js 官方提供了很多现成的着色器例子,在 examples/jsm/shaders/ 目录下。有空可以翻翻源码,看看大佬们怎么写。


互动

你写过最得意的 Shader 效果是啥?或者你在学习 Shader 时遇到过什么坑?评论区分享出来,咱们一起讨论 😏

下篇预告:【Three.js 后期处理进阶】用 Shader 实现自己的滤镜,让画面拥有电影质感

❌
❌