Three.js 的核心只有三个东西:Scene(舞台)、Camera(视角)、Renderer(画笔)。这三者构成了 3D 世界的"金三角"沟通闭环:Scene 负责"有什么",Camera 负责"怎么看",Renderer 负责"画出来"。这是我的学习日记 Day 1。我不会让你做"代码搬运工",而是帮你打通这个金三角的任督二脉。读完这篇,你不仅能转起一个立方体,更能明白屏幕上每一帧图像是如何诞生的。
今天 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个参数:
-
视野范围FOV(度),想象你站在原地不动: 1.1你转头 能看 360° 的全景 但你不转头、不转眼球 ,静止时双眼能看到的范围大约 120° 1.2Three.js 里的视野范围就类似这个"静止视野",但它是垂直方向 的角度: FOV = 30°:像眯着眼上下只能看一小条 FOV = 120°:像睁大眼睛,上下视野很开阔 1.3一句话记住:度数越大,看到的范围越广;度数越小,看到的范围越窄。一般用 60°~80° 最接近自然效果。
-
宽高比 :通常用 canvas 的宽/高 3.近平面和远平面: 3.1近平面:比方说我们日常在喝水的时候,瓶口离我们很近,但是视野里消失了,我们看不到,如果没有近平面限制,会出现穿模的情况。 3.2远平面:这个很好理解,就像我们眺望远方的时候,太远的山和房屋我们都看不到,不是因为不存在而是超过了我们呢的视野范围,就好比来雾霾的时候天气预报上的能见度差不多。设置远平面也是让我们的项目性能更好。太远的东西没必要一次性渲染。
-
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 元素。
- 获取画布
// canvas const canvas = document.querySelector('canvas.webgl');
- 创建场景
// 创建场景 const scene = new THREE.Scene();
- 创建物体
csharp
//创建物体
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 网格:网格 = 几何体(形状)+ 材质(外观)。它是真正被添加到场景里、能被相机拍到的物体,也就是穿上服装的完整演员。
- 定义画布尺寸
arduino
// 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 渲染器的输出分辨率。
- 创建相机(怎么看?)
arduino
// 创建相机
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.动画循环:让立方体自己转起来
ini
// 创建时钟对象,用于获取经过的时间
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;
ini
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 所有复杂效果的地基。地基打牢了,上面盖什么楼都稳。
⚡ 为什么用 type="module"?
两点:① 才能使用 import 引入 Three.js;② 会自动等待 HTML 加载完,确保能获取到 canvas 元素。
- 获取画布
// canvas const canvas = document.querySelector('canvas.webgl'); - 创建场景
// 创建场景 const scene = new THREE.Scene(); - 创建物体
csharp
//创建物体
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 网格:网格 = 几何体(形状)+ 材质(外观)。它是真正被添加到场景里、能被相机拍到的物体,也就是穿上服装的完整演员。
- 定义画布尺寸
arduino
// 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 渲染器的输出分辨率。
- 创建相机(怎么看?)
arduino
// 创建相机
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.动画循环:让立方体自己转起来
ini
// 创建时钟对象,用于获取经过的时间
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;
ini
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 所有复杂效果的地基。地基打牢了,上面盖什么楼都稳。