别再复制粘贴了,从零拆解 3D 场景的诞生过程

0 阅读7分钟

Three.js 的核心只有三个东西:Scene(舞台)、Camera(视角)、Renderer(画笔)。这三者构成了 3D 世界的“金三角”沟通闭环:Scene 负责“有什么”,Camera 负责“怎么看”,Renderer 负责“画出来”。这是我的学习日记 Day 1。我不会让你做“代码搬运工”,而是帮你打通这个金三角的任督二脉。读完这篇,你不仅能转起一个立方体,更能明白屏幕上每一帧图像是如何诞生的。

方块.gif 今天 Day 1,不复制粘贴,从零搭一遍这个金三角。上面是最终效果,下面是拆解过程。

1. 铁三角拆解:Scene → Camera → Renderer

1.Scene:一个3d虚拟容器,想在3d世界里放入一些比如立方体,球体,圆柱体等的物体,就需要放置在Scene中,打个比方前面提到Scene是舞台,也就是说你所需要放置的立方体(主角),以及配合主角的灯光,布景都需要站在这个舞台上。

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

2.Camera:摄像机Camera 决定了从哪个位置、以什么角度、用什么视野去看这个 3D 世界。没有 Camera,Scene 里放再多东西也“没人看”。 相机种类很多,我们这一篇只关注透视相机(PerspectiveCamera),透视相机的原理其实就和我们人眼看东西一样,近大远小。 // 创建相机 const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 1000); 创建相机时有3个参数:

  1. 视野范围FOV(度),想象你站在原地不动: 1.1你转头能看 360° 的全景 但你不转头、不转眼球,静止时双眼能看到的范围大约 120° 1.2Three.js 里的视野范围就类似这个“静止视野”,但它是垂直方向的角度: FOV = 30°:像眯着眼上下只能看一小条 FOV = 120°:像睁大眼睛,上下视野很开阔 1.3一句话记住:度数越大,看到的范围越广;度数越小,看到的范围越窄。一般用 60°~80° 最接近自然效果。

  2. 宽高比 :通常用 canvas 的宽/高 3.近平面和远平面: 3.1近平面:比方说我们日常在喝水的时候,瓶口离我们很近,但是视野里消失了,我们看不到,如果没有近平面限制,会出现穿模的情况。 3.2远平面:这个很好理解,就像我们眺望远方的时候,太远的山和房屋我们都看不到,不是因为不存在而是超过了我们呢的视野范围,就好比来雾霾的时候天气预报上的能见度差不多。设置远平面也是让我们的项目性能更好。太远的东西没必要一次性渲染。

  3. Renderer:是执行者。它拿着 Scene(有什么)和 Camera(怎么看),一帧一帧地把 3D 场景“画”到你面前的屏幕上(通常是 canvas 画布)。就好比我们过年在家里看春晚,电视机手机就是我们的canvas,春晚现场的舞台表演和摄影机分别是Scene和Camera,通过直播投屏到千家万户家中设备,Renderer就充当转播系统。把画面渲染到我们电视上(canvas),直播延迟类似于一帧一帧的概念。

2.接下来我们就开始制作我们第一个立方体。

小贴士:导入与初始化: npm install three

import * as THREE from 'three';

const canvas = document.querySelector('canvas.webgl');
<canvas class="webgl"></canvas>
<script type="module" src="./main.js"></script>

⚡ 为什么用 type="module"
两点:① 才能使用 import 引入 Three.js;② 会自动等待 HTML 加载完,确保能获取到 canvas 元素。

  1. 获取画布 // canvas const canvas = document.querySelector('canvas.webgl');
  2. 创建场景 // 创建场景 const scene = new THREE.Scene();
  3. 创建物体
//创建物体
const geometry = new THREE.BoxGeometry(1, 1, 1);
// 材质
const material = new THREE.MeshBasicMaterial({ color: 0xDE2E6E});
// 网格
const mesh = new THREE.Mesh(geometry, material);
// 将网格添加到场景中
scene.add(mesh);

代码片段中还提到材质和网格,下面简单介绍一下:

3.1 材质:材质决定了物体表面长什么样——颜色、粗糙度、金属感、是否反光、是否有纹理贴图,那前面的比喻来说,材质就是春晚演员身上穿着的服装,不同颜色不同面料。

3.2 网格:网格 = 几何体(形状)+ 材质(外观)。它是真正被添加到场景里、能被相机拍到的物体,也就是穿上服装的完整演员。

  1. 定义画布尺寸
// sizes
const sizes = {
    width: 800,
    height: 600
}

以上代码作用是:

4.1 初始化相机:在后续代码中,sizes.width / sizes.height 被用作 THREE.PerspectiveCamera 的纵横比(aspect ratio),确保相机视角比例与画布一致,防止图像变形。

4.2 设置渲染器大小renderer.setSize(sizes.width, sizes.height) 使用这些值来设定 WebGL 渲染器的输出分辨率。

  1. 创建相机(怎么看?)
// 创建相机
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height);
camera.position.z = 2; // 把相机往后挪 2 个单位,就像把水瓶拿远一点才能看清全貌
scene.add(camera);

这段代码干了三件事:造一台相机 → 把相机放到某个位置 → 把相机架在舞台上

第一行是在初始化相机,前面说到过参数含义。

第二行是通过position属性在z轴移动相机位置, 在 Three.js 的世界里:

  • X 轴:左右(→)
  • Y 轴:上下(↑)
  • Z 轴:前后(↗)

比如说想象你手里拿着一瓶水,瓶口正对着你的眼睛。

  • 这条从你眼睛到瓶底的直线,就是 Z 轴
  • 瓶子贴着眼睛 → 看不到全貌
  • 把瓶子拿远一点 → 整个水瓶都在视野里

camera.position.z = 2 就是在做这件事:把相机往后挪 2 个单位,让物体完整进入画面。

3.动画循环:让立方体自己转起来

// 创建时钟对象,用于获取经过的时间
const clock = new THREE.Clock();

// 配置渲染器
const renderer = new THREE.WebGLRenderer({
    canvas: document.querySelector('canvas.webgl')
});
renderer.setSize(sizes.width, sizes.height);

// 动画循环
const tick = () => {
    const elapsedTime = clock.getElapsedTime();
    mesh.rotation.y = elapsedTime;
    
    camera.lookAt(mesh.position);
    renderer.render(scene, camera);
    
    window.requestAnimationFrame(tick);
}

tick();

这段代码做了四件事:造一个时钟 → 配好画笔 → 定义动画流程 → 启动循环

第一步:造一个时钟

// 创建时钟对象,用于获取经过的时间 const clock = new THREE.Clock(); Clock是three.js中自带的获取时间的工具,他能告诉我们动画开始后过去了多长时间,就好比春晚开始后可以随时知道已经开始了多久?

第二步:配置“画”笔

// 配置渲染器 const renderer = new THREE.WebGLRenderer({ canvas: document.querySelector('canvas.webgl') }); renderer.setSize(sizes.width, sizes.height);

这段代码就相当于告诉渲染器,往哪个画布上渲染,我们页面元素是canvas.webgl,设置画布尺寸,就比如春晚表演中摄像师调好设备要考虑连接到哪块屏幕(canvas)、屏幕多大尺寸(setSize)。

第三步:定义动画核心

` // 动画循环 const tick = () => { const elapsedTime = clock.getElapsedTime(); mesh.rotation.y = elapsedTime;

camera.lookAt(mesh.position);
renderer.render(scene, camera);

window.requestAnimationFrame(tick);

} `

这是一个递归函数——它会在每一帧调用自己,形成无限循环。

代码作用春晚比喻
clock.getElapsedTime()问“现在开场几秒了?”导演看秒表
mesh.rotation.y = elapsedTime让立方体根据时间旋转演员按节拍转身
camera.lookAt(mesh.position)相机一直盯着立方体摄像机追着主角拍
renderer.render(scene, camera)把这一帧画出来把画面投到大屏
requestAnimationFrame(tick)预约下一帧继续执行告诉导播“下一帧继续拍”

看到没?动画的本质就是这样:每一帧都问一遍时间、改一遍物体、重新渲染一次。每秒重复 60 次,就成了你看到的流畅旋转。

第四步:完成无限旋转

tick();

定义完 tick 函数后,调用它一次。它会在内部无限循环下去。

一句话总结动画的完整流程就是: 时钟记录时间 → 物体随时间旋转 → 相机盯着物体 → 渲染器画出这一帧 → 预约下一帧 → 无限循环。

恭喜!你已经完成了 Three.js 的第一个里程碑:

  • ✅ 理解了 Scene、Camera、Renderer 的金三角关系
  • ✅ 创建了第一个 3D 立方体
  • ✅ 让立方体自己转了起来

这三件事听起来简单,但它们是 Three.js 所有复杂效果的地基。地基打牢了,上面盖什么楼都稳。