1.1 装配线总览:三件套与首件样板 本节摘要:Three.js 场景由场景、渲染器、相机三件套装配而成,网格由几何体与材质拼装。本节按装配工序拆解各部件的职责边界,给出首件样板的最短可运行代码,并教你用"逐一拆除法"诊断黑屏。它承接导读的全册地图,是后续所有工位的地基。 从一块黑屏说起 为什么那么多初学者的第一段 Three.js 代码跑出来是纯黑?因为三件套里任何一件没接好,结果都一样:什么都不显示。渲染器没挂到页面,浏览器里根本没有画布;场景里没放东西,画布上自然无物可画;相机没对准物体,画出来的就是空视野;渲染循环没启动,哪怕前三步全对,画面也停留在第 0 帧。黑屏不是一种故障,是四种故障共用的一个表象——本节结束时,你应该能像老师傅听声音辨故障一样,看代码就说出哪道工序断了。
本节摘要:Three.js 场景由场景、渲染器、相机三件套装配而成,网格由几何体与材质拼装。本节按装配工序拆解各部件的职责边界,给出首件样板的最短可运行代码,并教你用"逐一拆除法"诊断黑屏。它承接导读的全册地图,是后续所有工位的地基。
为什么那么多初学者的第一段 Three.js 代码跑出来是纯黑?因为三件套里任何一件没接好,结果都一样:什么都不显示。渲染器没挂到页面,浏览器里根本没有画布;场景里没放东西,画布上自然无物可画;相机没对准物体,画出来的就是空视野;渲染循环没启动,哪怕前三步全对,画面也停留在第 0 帧。黑屏不是一种故障,是四种故障共用的一个表象——本节结束时,你应该能像老师傅听声音辨故障一样,看代码就说出哪道工序断了。
先把站位图立起来,后文所有章节的部件都会挂到这张图上。

场景是一个容器对象,真实的职责是维护一棵"物件树":调用 add 方法把网格、灯光塞进去,它们就以子节点的身份挂到树上。注意场景本身没有任何视觉属性,它不渲染、不显示、不占像素,你永远"看不到"场景,只能看到它装的东西。所以排查黑屏的第一问是:地台上到底装没装货。
渲染器是唯一和浏览器打交道的部件。它创建一块画布(canvas 元素),把场景在当前相机视角下的画面算成像素写进画布。WebGLRenderer 的初始化参数里,antialias 决定边缘是否抗锯齿,alpha 决定背景是否透明;setSize 决定画布尺寸,setPixelRatio 决定物理像素密度——这颗参数在第 8 章性能质检里还会反复出现,高像素比的设备上把它硬设为 2 以上,帧预算会先崩在这里。
相机不渲染任何东西,它只是描述"从哪里、用什么视野看"。它同样可以不加入场景(除非希望它随场景一起变换),渲染器每次绘制都会向它要一份视图矩阵。下一章布光时会看到相机还有个隐藏身份:聚光灯与阴影相机都借用了它的"取景"机制。
网格是几何体与材质的拼装件:几何体提供顶点数据(有哪些点、怎么连成三角形),材质提供表面数据(什么颜色、吃不吃光)。同样的几何体换材质,外观立变;同样的材质换几何体,形状立变——两者解耦,正是装配线能分工作业的原因。
背景:电子沙盘原型,要求一块深色底面上有一个缓慢自转的绿色立方体,代码要短到能贴在工单回复里,但每一行都要讲得出归属。
操作:按工位顺序装配,注释里标出每行属于哪道工序。
// 工位 A:建地台 const scene = new THREE.Scene(); scene.background = new THREE.Color(0x202428); // 深色底面,客户指定 // 工位 C:架机位(视野角 75 度、宽高比、近裁剪 0.1、远裁剪 1000) const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); camera.position.z = 5; // 机位沿 z 轴后退 5 个单位,正对原点 // 工位 D-1:接动力电,创建渲染器并挂到页面 const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // 工位 B:下料并拼装首件 const geometry = new THREE.BoxGeometry(1, 1, 1); // 形状:边长 1 的立方体 const material = new THREE.MeshBasicMaterial({ color: 0x3fa34d }); // 表现:不吃光的纯色 const cube = new THREE.Mesh(geometry, material); // 拼装成网格 scene.add(cube); // 上台 // 工位 D-2:启动渲染循环,每帧自转一次再出图 function animate() { requestAnimationFrame(animate); cube.rotation.x += 0.01; cube.rotation.y += 0.01; renderer.render(scene, camera); } animate();
结果:浏览器中出现深色底面上自转的绿色立方体,边缘平滑无锯齿。改一下 camera.position.z 为 2,立方体会撑出画面边缘;改成 20,它缩成一粒豆子——机位远近对画面构图的影响,比任何参数讲解都直观。
解读:这段代码里最容易被跳过理解的是 PerspectiveCamera 的四个参数。75 度是竖直视野角,数字越大装进画面的范围越宽、近处物体越显畸变;宽高比必须与画布一致,否则画面会被拉伸变形;0.1 与 1000 是近远裁剪面,只有落在两个平面之间的物件才会被绘制,near 取得过大会把贴近机位的物件整个裁没。MeshBasicMaterial 不参与光照计算,所以下一章换灯之前,它是最不容易出错的"素坯漆"。
变式:把 BoxGeometry 换成 1.3 节要讲的 SphereGeometry(1, 32, 16),其余一行不改,样板就从方块变成球——几何体与材质解耦的价值立刻显现。再进阶一步:在循环里累计一个时间变量,用正弦函数让立方体的 y 位置随时间上下浮动,为第 6 章的属性动画预热。
与其背排查口诀,不如故意制造四种黑屏各看一遍。其一,注释掉 scene.add(cube):黑屏,因为地台无货。其二,保留 add 但注释 camera.position.z = 5:相机默认在原点,立方体边长 1 而近裁剪面 0.1——相机"在物体内部",只看到某个面的内侧,画面几乎全黑。其三,注释 appendChild 那行:页面空白,画布根本没挂上去。其四,注释 animate() 调用:依然黑屏,哪怕前三步全对,循环没启动就没有第一帧。四个实验做完,"黑屏四查"就从口诀变成手感了。
下一节先把车间接好电:模块化引入、尺寸自适应与调试台,工位牢了,后面八成的怪问题都不会发生。