☰
《Threejs入门》Ch2 三要素:场景、相机、渲染器
2026/10/20 2:16:57 网站建设 项目流程

本篇:第 1 篇 · 走进 3D | 配套示例:examples/ch02/index.html

本章目标

  1. 能说出 scene、camera、renderer 各自负责什么,并解释renderer.render(scene, camera)为什么必须同时带两个参数。
  2. 记住 three.js 的空间约定:右手坐标系、y 轴朝上,能用AxesHelper读懂一个场景里"东西在哪、朝哪放"。
  3. 不看参考能默写出三要素样板代码(含setSize和setPixelRatio两行容易被漏掉的设置)。

上一章你"看见"了画面但没有"看懂"代码。这一章把examples/ch01那 20 行拆成零件,同时建立 three.js 世界里最重要的一块地基——坐标系。后面第 2 篇的数学基础,全部从它出发。

2.1 一个比喻记住三要素

把渲染一帧画面想象成拍一张照片:

代码里的对象片场里它是职责边界
THREE.Scene拍摄现场(地块)一个容器:所有要被拍到的东西都必须摆进这个现场
THREE.Mesh现场里的道具一个具体物体 = 形状(geometry)+ 外观配方(material)
THREE.PerspectiveCamera摄影机决定从哪看、朝哪看、看多宽,它不渲染任何东西
THREE.WebGLRenderer摄影师 + 冲洗设备把"现场 + 机位"算成一张图像,写到<canvas>上

注意"职责边界"这栏的三个措辞,它们是后面很多 bug 的诊断入口:物体不add进 scene 就等于没摆进片场——摄影师拍不到;camera 只管取景不管合成;renderer 是唯一的"输出端",一个 Scene 可以喂给多个 renderer(比如做画中画、缩略图),一个 camera 也可以在不同时刻拍不同 scene。

2.2 坐标系:这个世界的门牌号

3D 场景里一切位置都是"门牌号",必须先约定怎么读门牌。three.js 的约定是右手坐标系,y 轴朝上,默认单位为"1 = 1 米"(单位不是强制的,是社区惯例,见 Ch1 实验 1)。

y(上) │ │ │ │________ x(右) ╱ ╱ z(朝向屏幕外的你)

用右手判定:四指从 x 轴弯向 y 轴,拇指指向的就是 z 轴。three.js 里**+z 指向屏幕外、朝向观众**,摄像机默认放在 +z 一侧往原点看。这一点与许多软件不同,换工具时要心里有数:

软件/标准上方向备注
three.js / OpenGLy 朝上,-z 为相机视线方向本书世界
Blender / 游戏引擎常见z 朝上导出模型进 three.js 要做轴向转换(第 19 章)
网页 CSS / 图像坐标y 朝下所以贴图 UV 的 v 轴方向经常"反"(第 8 章)

怎么亲眼确认?用AxesHelper——在场景里放一个,画面角上会出现红绿蓝三根短线,红=x,绿=y,蓝=z(RGB 顺序对应 XYZ)。配套的还有GridHelper(地面网格)和CameraHelper(把相机视锥画出来)。它们只用于开发调试,正式发布前删掉。本章示例里这三个都有,下一章开始我们就不用了——但任何时候画面不对,先摆一个 AxesHelper 看方向,这是 3D 开发的第一诊断动作。

2.3 逐行拆解:那 20 行代码

按执行顺序,分四组。每组末尾有"现在只需/第 N 章深讲"的标注——这是本书对复杂度的显式管理。

组一:现场

constscene=newTHREE.Scene();

Scene 就是一个容器对象,本身不可见、不产生任何像素。它还顺手管着两样全局设定:scene.background(背景,可赋颜色或贴图)和scene.fog(雾效,让远处物体逐渐融进背景色)。本章示例用到了这两个。——fog 与背景贴图第 15 章见。

组二:机位(一个相机需要的全部:位置 + 朝向)

constcamera=newTHREE.PerspectiveCamera(45,innerWidth/innerHeight,0.1,100);camera.position.set(3,2,4);camera.lookAt(0,0,0);

PerspectiveCamera构造函数的四个参数,逐个对应"摄影机怎么架":

  1. fov(视场角)= 45:镜头纵向张开多少度。越小越"望远"(画面被拉近、空间感压缩),越大越"广角"(夸张的近大远小、鱼眼边缘畸变)。人眼舒适区间大约 45~75。
  2. aspect(宽高比)= 内宽/内高:画面宽高比必须等于 canvas 的实际宽高比,否则圆球渲染成椭圆——这是常见坑 2 的前半段,窗口变化时还要同步更新(示例里做了resize监听)。
  3. near(近裁剪面)= 0.1:比这更近的物体不画。它必须大于 0,且相机离物体比 near 还近时物体会消失(常见坑 1 的另一半)。
  4. far(远裁剪面)= 100:比这更远的不画。near 与 far 之间围成一个梯形"取景空间",叫视锥(frustum)——流水线第 ① 站之后、第 ② 站剔除时,GPU 真的在拿视锥逐个三角形做检查。

position.set(3, 2, 4)与lookAt(0, 0, 0):架机器要拧两颗螺丝——摆在哪(位置)、镜头朝哪(朝向)。相机和所有物体一样生活在 2.2 节的坐标系里。少了lookAt,相机默认只朝着自己的 -z 方向,画面十有八九是空的。

为什么透视相机参数这么重要?因为 fov/near/far 三个数最终会被做成一个 4×4 投影矩阵,第 11 章你把它完整推导出来,现在先把参数含义用熟。

组三:道具

constcube=newTHREE.Mesh(newTHREE.BoxGeometry(1,1,1),newTHREE.MeshNormalMaterial());scene.add(cube);

Mesh= 网格 = 形状 + 外观,两个零件各管一半:BoxGeometry是"一个 1×1×1 米盒子的三角形清单",MeshNormalMaterial是"用面朝向涂色的配方"。new完不scene.add等于道具造好了没搬进片场——画面空白,新手第一大坑(第 7、8 章把 geometry 和 material 拆开深讲)。

组四:摄影师

constrenderer=newTHREE.WebGLRenderer({canvas:document.getElementById('stage'),antialias:true});renderer.setSize(innerWidth,innerHeight);renderer.setPixelRatio(Math.min(devicePixelRatio,2));renderer.setClearColor('#11151c');
  • 构造参数里的canvas:告诉 renderer 往哪个<canvas>元素上画。不传它会自己造一个 canvas 塞进 body,布局会乱。
  • setSize:设置 canvas 的绘制尺寸(回忆 Ch1 坑 3:尺寸只听它的)。
  • setPixelRatio:高分屏(Retina、现代手机)一个 CSS 像素对应 2~3 个物理像素,不设会糊;用Math.min(devicePixelRatio, 2)封顶是行业惯例——再高的像素比带来的清晰度收益抵不上 GPU 开销(第 20 章算这笔账)。
  • antialias: true:打开抗锯齿,三角形边缘不再呈台阶状。

快门

renderer.render(scene,camera);

一条命令把流水线走一遍:从 camera 的视点,把 scene 算成 canvas 上的一张图。必须同时有两个参数——现场和机位缺一不可:同一个 scene 换机位就是另一张照片。上一章setAnimationLoop里每帧执行一次它,就是每秒按 60 次快门。

2.4 最小可运行示例:把坐标系摆进画面

examples/ch02/index.html在本章知识点上做了三件事:地面网格、坐标轴、三块颜色不同且摆在坐标正方向上的板子,每个对象一眼对应一条轴:

scene.add(newTHREE.AxesHelper(5));// 红x 绿y 蓝z,长5米constgrid=newTHREE.GridHelper(10,10);// 10米见方、10格的xOz地面网格scene.add(grid);constplane=(color)=>newTHREE.Mesh(newTHREE.PlaneGeometry(1,1),newTHREE.MeshBasicMaterial({color,side:THREE.DoubleSide}));constredOnX=plane('red');redOnX.position.set(1.5,0.5,0);constgreenOnY=plane('lime');greenOnY.position.set(0,1.5,0);greenOnY.rotation.y=Math.PI/2;constblueOnZ=plane('dodgerblue');blueOnZ.position.set(0,0.5,1.5);scene.add(redOnX,greenOnY,blueOnZ);

运行它,对着画面做一件事:说出绿色板子为什么被rotation.y = π/2转了一下。答案:PlaneGeometry默认朝 +z(它的法线是 z 轴),不转的话我们从 -z 一侧的相机看它是一条边;绕 y 轴转 90° 后朝向 x 轴方向,才能被看到正面。——这就是 2.2 节坐标系 + "面有朝向"的第一次配合。DoubleSide是双保险(正面背面都画,第 8 章再谈要不要用)。

2.5 改造实验

  1. 删掉scene.add(cube)(本章示例则删greenOnY):那块东西消失,其余照旧。确认"容器决定画什么"。
  2. camera.position.set(0, 0.5, 0.05)保持lookAt(0,0,0):几乎怼在原点。结果一片空——相机进入了 near 裁剪面以内,所有物体都"太近"被裁掉了。这是坑 2 的实体化。
  3. fov改成 100:退后一点再对比,感受广角的边缘拉伸;改成 15,远处的东西"黏"到近处物体上(空间压缩)。这个手感在第 11 章推导完投影矩阵后会有理论解释。
  4. 把浏览器窗口从宽拉成窄,观察画面:示例里resize监听器会同步camera.aspect。把监听器删掉再试:窗口变化时圆变椭圆、板子变形——aspect 错了相机不知道,它只会用上一次构造时的比例画。

2.6 常见坑

坑 1:新new的物体没add进 scene。画面空白或"少了某样东西",第一反应永远是检查 add。

坑 2:相机太靠近物体,或 near 设得太大。现象是"看进去全是黑/背景色"。相机不是无限穿透的:它有一个贴脸的最近取景距离 near。排查思路:把 near 改到 0.01 试试,或把相机退远。

坑 3:窗口尺寸变了只改setSize、不改camera.aspect。画面拉伸、比例失真。正确样板(已在 ch02 示例内,抄走即用):

window.addEventListener('resize',()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();// 必须调,参数改完不会自动生效renderer.setSize(innerWidth,innerHeight);});

坑 4:误以为 camera 是"玩家的视角"。camera 只是取景框,与"你在场景里走到哪"无关——移动相机 ≠ 移动角色。第一人称、第三人称是"谁跟着谁"的关系(第 23 章游戏实战展开)。

2.7 练习

  1. 默写题:不看书写出三要素初始化 + 一次render的完整代码(一个红立方体、相机在 (2,2,5) 看原点、背景深灰)。写完再对照——重点检查setSize那行和scene.add有没有漏。
  2. 预测题:camera.position.set(0, 0, 10),lookAt(0, 0, 0),一个位于 (3, 2, 4) 的 1×1 立方体会出现在画面哪个方位?为什么?(提示:相机在 +z 看原点,相机自己的 x 轴、y 轴各朝哪?)
  3. 动手题:给示例加上scene.fog = new THREE.Fog('#11151c', 10, 50),把相机挪到(20, 10, 20),观察远处板子发生了什么;再挪回来。说出 fog 的两个数字参数分别是什么距离。

参考答案

  1. 略——自查点:PerspectiveCamera四参数、Mesh两参数、scene.add(mesh)、renderer.setSize。
  2. 画面偏右上。相机在 (0,0,10) 看向原点时视线方向是 -z,此时它的右方恰好是世界 +x、上方是 +y(对照 ch01 示例的默认机位即可印证)。所以 x=3 出现在画面右侧,y=2 出现在上方,z=4 让它离相机更近一些、显得更大。这道题的要点:相机在 +z 一侧朝 -z 看时,"屏幕左右"和"世界 x"并不反号;只有当你把相机移到 z 轴负侧往回看时,左右才会互换——画面摆位时先想清楚相机视线方向,根源就在这一层。
  3. fog 两个参数是开始距离 10、完全遮蔽距离 50:距离相机 10 米内不受雾影响,50 米外融成背景色。相机拉远后板子逐渐消失,是因为它们相对相机距离进了雾区间——fog 的参照是相机距离,不是世界坐标。

本章小结

  • 三要素分工:scene 是容器(不被 add 就不存在),camera 是取景框(位置+朝向两颗螺丝),renderer 是唯一输出端(尺寸只归setSize/setPixelRatio管)。
  • render(scene, camera)每次调用 = 整条图形管线走一遍;动画就是反复按快门。
  • 世界是右手坐标系、y 朝上;AxesHelper(红x绿y蓝z)是第一诊断工具。
  • PlaneGeometry默认朝 +z,物体"看不见"很多时候是朝向问题——朝向由面的法线决定,法线是向量,向量是下一章的主角。

从 Ch3 起进入第 2 篇数学基础。第一章就说过,这些数学不是选读:本章你已经两次碰到它的影子(朝向/法线、相机参数背后的投影矩阵)。下一站:向量。

本章完整源码

本章示例的全部源码都在下面,逐字复制即可重建,不需要另外下载。每个代码块都是examples/里对应文件的原样副本(由脚本生成并逐字节对账),所以你抄下来的就是作者这边跑通过的那一份。

把下面这个文件逐字存成examples/ch02/index.html,然后在examples/的上一级目录里起一个静态服务器:

python-mhttp.server8000

再访问http://127.0.0.1:8000/examples/ch02/。three 由 importmap 从 unpkg 取,要联网;版本号保持 0.186.1,换版本先回看 Ch1 的版本纪律。

文件清单

ch02/index.html (76 行)

ch02/index.html

<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Ch2 · 三要素与坐标系</title><style>html, body{margin:0;height:100%;overflow:hidden;background:#11151c;}#stage{display:block;}</style><scripttype="importmap">{"imports":{"three":"https://unpkg.com/three@0.186.1/build/three.module.js","three/addons/":"https://unpkg.com/three@0.186.1/examples/jsm/"}}</script></head><body><canvasid="stage"></canvas><scripttype="module">import*asTHREEfrom'three';// ── 组一:现场 ──constscene=newTHREE.Scene();scene.background=newTHREE.Color('#11151c');// ── 组二:机位 ──constcamera=newTHREE.PerspectiveCamera(45,innerWidth/innerHeight,0.1,100);camera.position.set(4,3,6);camera.lookAt(0,1,0);// ── 组三:道具 ──scene.add(newTHREE.AxesHelper(5));// 红=x 绿=y 蓝=z,长 5 米constgrid=newTHREE.GridHelper(10,10);// xOz 地面网格:10 米见方、10 格scene.add(grid);constplane=(color)=>newTHREE.Mesh(newTHREE.PlaneGeometry(1,1),newTHREE.MeshBasicMaterial({color,side:THREE.DoubleSide}));constredOnX=plane('red');redOnX.position.set(1.5,0.5,0);constgreenOnY=plane('lime');greenOnY.position.set(0,1.5,0);greenOnY.rotation.y=Math.PI/2;constblueOnZ=plane('dodgerblue');blueOnZ.position.set(0,0.5,1.5);scene.add(redOnX,greenOnY,blueOnZ);// ── 组四:摄影师 ──constrenderer=newTHREE.WebGLRenderer({canvas:document.getElementById('stage'),antialias:true,});renderer.setSize(innerWidth,innerHeight);renderer.setPixelRatio(Math.min(devicePixelRatio,2));// 坑 3 的正确样板:aspect 与尺寸同步window.addEventListener('resize',()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight);});renderer.setAnimationLoop(()=>{renderer.render(scene,camera);});</script></body></html>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询