如果你小时候玩过积木,那你其实已经懂 Three.js 了。想象一下:你把一块块方积木垒成小房子,给屋顶贴上蓝色贴纸,在小窗户里放一盏小灯,然后围着房子转圈看它——这个“搭积木、贴贴纸、放灯光、绕着看”的过程,正是 Three.js 在浏览器里做的事。只不过,你的积木块变成了立方体、球体、圆柱体这些几何体,贴纸变成了材质贴图,灯光变成了虚拟光源,而“围着看”则是通过一个可移动的虚拟相机完成的。Three.js 就像一盒精美的积木套装:它帮你封装好了最复杂的几何计算和渲染管线,你只需要专注于“搭什么”和“怎么搭”,而不是从零去造一块积木。这套工具由 Three.js 团队维护,基于 WebGL,让浏览器无需任何插件就能渲染出令人惊叹的 3D 场景。

岗位关联

掌握 Three.js,等于拿到了打开“三维互联网”大门的钥匙。无论你未来做数据可视化、在线游戏、产品展示还是元宇宙方向,这项技能都能让你从众多前端开发者中脱颖而出。

目录
  1. 可视化演示
  2. 场景舞台搭建
  3. 相机视野控制
  4. 几何体积木
  5. 材质与灯光
  6. 动画渲染循环
  7. 交互与碰撞
  8. 性能优化策略
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

场景舞台搭建

想象你刚拆开一盒积木,第一件事是什么?当然是找一块平整的底板——只有把底板铺好,你才知道往哪儿放积木、积木之间怎么连接。在 Three.js 里,这块“底板”就是 Scene(场景)。它是所有对象的容器,你创建的每一个积木(几何体)、贴纸(材质)、灯光都必须放进这个容器里,否则就像把积木放在地上——虽然存在,但不在你的“积木房子”里。

当你 new THREE.Scene() 创建一个场景时,Three.js 会在内存中维护一棵对象树。你可以把任何可见或不可见的对象挂到这棵树上,就像往底板上堆叠积木。场景还自带一套坐标系——右手坐标系,其中 X 轴向右、Y 轴向上、Z 轴朝向你。这套坐标系就像是底板上的刻度线,让你能精确地说出“这块积木放在距离中心 5 格、高度 3 格的位置”。

Scene 对象最核心的几个能力包括:add() 方法像一只手,把积木稳稳放到底板上;remove() 方法像拆积木,需要时也能干净地拿走;traverse() 方法像你的眼睛,可以在场景里巡视每一个节点。需要注意的是,Three.js 的 Scene 继承自 Object3D,这意味着它本身也可以拥有位置、旋转、缩放属性——这在实现“滚动的天空盒”或“动态世界”时非常有用。

1. 创建场景:Scene 是最顶层的容器,所有对象都要挂载进来。

scene=new Scene()\text{scene} = \text{new Scene}()

2. 添加对象:通过 add 方法把物体挂到场景树下,此时物体才进入渲染范围。

scene.add(object)\text{scene}.\text{add}(\text{object})

3. 对象树结构:每个场景都是一棵层级树,根节点是 Scene,子节点可以是 Mesh、Light、Camera 等。

Scene→Mesh→Material\text{Scene} \rightarrow \text{Mesh} \rightarrow \text{Material}

把场景想象成一张无限大的积木底板,这解释了为什么 Three.js 的性能瓶颈往往不在场景本身,而在于场景里的对象数量。合理组织场景树,是 3D 应用优雅与高效的起点。

相机视野控制

搭好积木房子后,你想从各个角度欣赏它。你会怎么做?蹲下来从窗户看、站到凳子上俯视、或者绕着一圈走。在 Three.js 里,你的“眼睛”就是 Camera(相机)。它是一个抽象的视点,决定了“从哪个位置、朝哪个方向、以多大视野”去看场景。没有相机的 Three.js 应用是黑屏的——就像闭着眼睛搭积木,东西都在,但你看不见。

Three.js 最常用的两种相机是 PerspectiveCamera(透视相机)和 OrthographicCamera(正交相机)。透视相机模拟人眼和真实相机的特性:近大远小,现实生活中我们看到的世界就是这样的。正交相机则保持物体大小恒定,不管距离远近,就像建筑图纸的平面视图。透视相机适合游戏和产品展示,正交相机适合 2.5D 界面和建筑平面图。其核心参数包括视野角度(FOV)、宽高比(aspect)、近裁剪面(near)和远裁剪面(far)。

相机的定位也讲究:position.set(x, y, z) 确定相机站在哪里,lookAt(x, y, z) 让相机看向哪里。这就像你站在积木房子东南角,视线聚焦在房屋正门的把手上。设置好这两个参数,就等于确定了“摄影师的位置和镜头朝向”。

1. 近大远小:透视矩阵把三维坐标投影到二维屏幕,核心是视锥体变换。

Pproj=[faspect0000f0000far+nearnear−far2⋅far⋅nearnear−far00−10]\displaystyle P_{\text{proj}} = \begin{bmatrix} \frac{f}{\text{aspect}} & 0 & 0 & 0 \\ 0 & f & 0 & 0 \\ 0 & 0 & \frac{\text{far}+\text{near}}{\text{near}-\text{far}} & \frac{2\cdot \text{far}\cdot \text{near}}{\text{near}-\text{far}} \\ 0 & 0 & -1 & 0 \end{bmatrix}

2. 定义视野:fov 是垂直方向的视野角度,单位是度。

f=1tan⁡(fov⋅π360)f = \frac{1}{\tan(\frac{\text{fov} \cdot \pi}{360})}

3. 裁剪范围:near 和 far 定义了相机能看到的最远和最近距离,超出范围的物体被裁剪掉。

near<z<far\text{near} < z < \text{far}

相机就像你的双眼,决定了你看到一个怎样的 3D 世界。调好相机参数,是每个 Three.js 应用的第一步——它不是技术题,而是视觉艺术题。

几何体积木

现在进入最有趣的环节:往底板上搭积木。积木盒里有各种各样的形状——立方体、球体、圆柱体、圆锥体、环面……在 Three.js 中,这些形状被称为 Geometry 或 BufferGeometry。它们是 3D 场景的最小积木单元,通过组合它们,可以搭建出几乎任何你想要的物体:房子、汽车、人物,甚至整个城市。

Three.js 内置了大量基础几何体,就像积木盒里的标准件。BoxGeometry(宽,高,深) 是正方体,SphereGeometry(半径,分段数) 是球体,CylinderGeometry(半径顶,半径底,高度) 是圆柱体。每个几何体都由顶点(vertices)和面(faces)构成——顶点是积木块的角落,面是积木块的表面。当你创建一个 BoxGeometry 时,Three.js 会计算好 8 个顶点、6 个面(每个面由 2 个三角形组成),然后把这些数据以 BufferGeometry 的形式存在内存中。

BufferGeometry 是 Three.js 从 r125 版本开始主推的几何体格式。它把顶点位置、法线、颜色、UV 坐标等数据按二进制连续存储在缓冲区中,GPU 可以直接读取并进行渲染。它的优势在于高效——就像积木块表面提前印好了所有贴纸的定位孔,拼搭时直接对齐就行。而旧的 Geometry 由于数据分散、操作灵活但效率较低,在新版本中已被移除。

1. 定位顶点:每个顶点都有三维坐标,定义积木的角落位置。

position=(xi,yi,zi)\text{position} = (x_i, y_i, z_i)

2. 连接顶点成三角形:index 数组定义每三个顶点组成一个三角形面,GPU 用它来渲染表面。

indices=[(i1,i2,i3),(i4,i5,i6),… ]\text{indices} = [(i_1, i_2, i_3), (i_4, i_5, i_6), \dots]

3. 法线计算:每个顶点都有一个法线向量,它决定光照在表面上的反射效果。

n=(v2−v1)×(v3−v1)∥(v2−v1)×(v3−v1)∥\mathbf{n} = \frac{(v_2 - v_1) \times (v_3 - v_1)}{\|(v_2 - v_1) \times (v_3 - v_1)\|}

积木块是 3D 世界的根基。理解了顶点、面、缓冲区,你就理解了 GPU 如何“看见”这个世界——而不是被 Three.js 的 API 牵着走。这是从“会用”迈向“懂行”的关键一步。

材质与灯光

积木块的形状有了,但它们还是灰蒙蒙的——就像刚出厂的素色积木,没有贴纸、没有颜色。要让积木真正“活”起来,需要两样东西:Material(材质)和 Light(灯光)。材质是积木的皮肤,决定了物体表面如何反射光线;灯光是房间里的灯,它照亮积木,让皮肤显现出颜色和质感。

Three.js 内置了多种材质,就像不同风格的贴纸。MeshBasicMaterial 是最简单的“纯色贴纸”,它不依赖光照,只用纯色绘制,适合做背景和辅助线。MeshLambertMaterial 是“哑光贴纸”,表面不反光,适用于粗糙的物体(如墙壁、泥土)。MeshPhongMaterial 是“亮光贴纸”,带高光反射,适合塑料、金属质感。MeshStandardMaterial 是“物理贴纸”,基于 PBR(物理渲染),效果最真实,适合写实风格。

光源则是场景中的“灯”。AmbientLight 是环境光,像天花板上的吸顶灯,均匀照亮全屋;DirectionalLight 是平行光,像太阳,以固定方向照射;PointLight 是点光源,像一盏台灯,从一点向四面八方发亮;SpotLight 是聚光灯,像舞台追光灯,形成一束光锥。不同材质对光的反应完全不同——MeshBasicMaterial 完全不理会光源,而 MeshStandardMaterial 会综合计算漫反射、镜面反射和环境光照。

1. 漫反射(Lambert 模型):粗糙表面的反射强度与光线入射角的余弦成正比。

Ld=kd⋅I⋅max⁡(0,n⋅l)\displaystyle L_d = k_d \cdot I \cdot \max(0, \mathbf{n} \cdot \mathbf{l})

2. 高光反射(Phong 模型):光滑表面的高光强度与反射向量和视线向量的夹角有关。

Ls=ks⋅I⋅max⁡(0,r⋅v)p\displaystyle L_s = k_s \cdot I \cdot \max(0, \mathbf{r} \cdot \mathbf{v})^p

3. PBR 能量守恒:表面的反射率与折射率之和不超过 1,保证物理正确。

R+T≤1R + T \le 1

材质和灯光就像积木的“颜值担当”。即使几何体再精细,没有好的材质和灯光,场景依然显得廉价;反之,一个球体加一束灯光,也能呈现电影级的质感。这就是 3D 渲染的魅力。

动画渲染循环

积木房子搭好了、贴纸贴好了、灯光也亮了,但它是静止的——像一张照片。照片很好看,但我们想要的是“电影”。如何让积木旋转起来、让小车动起来、让小鸟飞行?答案是“动画渲染循环”(render loop)。Three.js 的动画机制很简单:不断更新物体状态,然后重新渲染场景。

这个循环的核心是 requestAnimationFrame 函数,它是浏览器提供的“节拍器”。它告诉浏览器:“在下一帧绘制时,请调用这个函数。”浏览器会在每次屏幕刷新时(通常是每秒 60 次)调用它,从而实现流畅的动画。Three.js 的 renderer.render(scene, camera) 可以把这个循环封装得极其简洁。

在实际项目中,你还需要一个关键工具:Clock(时钟)。它能精确计算两帧之间的时间差(delta time),这个差值正好可以用来驱动运动。为什么要用 Clock?因为不同设备的刷新率不同——一台 120Hz 的显示器和一台 60Hz 的显示器,如果每帧旋转固定角度,动画速度就会差一倍。而把它设置为“每秒旋转 60 度”(rotation += 60 * delta),就能保证在任何设备上都以相同的速度旋转。

1. 计算时间增量:Clock.getDelta() 返回距上一次调用该函数的时间间隔(秒)。

Δt=t当前−t上一帧\Delta t = t_{\text{当前}} - t_{\text{上一帧}}

2. 更新位置:每帧根据速度乘以时间增量来计算位移量,保证动画速度与帧率无关。

position=position+v⋅Δt\text{position} = \text{position} + v \cdot \Delta t

3. 渲染新画面:调用渲染器,将最新的场景状态绘制到屏幕上。

Render(scene,camera)\text{Render}(\text{scene}, \text{camera})

动画循环就是 3D 世界的“心跳”。有了这个心跳,场景中的一切才能活起来。理解 requestAnimationFrame 和 Clock 的关系,你就掌握了让 3D 世界“动起来”的魔法钥匙。

javascript6 行
function animate() {  requestAnimationFrame(animate);  mesh.rotation.x += 0.01;  // 更新积木的旋转  renderer.render(scene, camera);  // 重新渲染整个世界}animate();

交互与碰撞

一个会动的 3D 场景很酷,但如果用户只能看、不能摸,那就像一个不允许触摸展品的博物馆——精美但缺乏参与感。要让用户“摸”到积木,就需要交互。Three.js 中最核心的交互工具是 Raycaster(射线检测器)、OrbitControls(轨道控制器),以及用于物理反馈的碰撞检测。

Raycaster 的原理其实非常简单:从相机(或鼠标)位置发射一条看不见的射线,检测这条射线在空间中穿过了哪些物体。就像你闭上一只眼、用手指指向一个方向,你知道手指在那条线上有多远。Raycaster 会返回被射线击中的所有物体列表,并按距离排序。这样用户点击屏幕上的一个点,你就能知道“用户点击了哪个积木”。

OrbitControls 是 Three.js 最常用的相机控制工具,它让用户可以用鼠标拖拽旋转视角、滚轮缩放、右键平移。它就像是给相机装上了一台“云台”,用户可以直接操控自己的视角。看似复杂的功能,用起来只需几行代码:

当交互变得复杂(比如用户拖拽积木到另一个积木上),就需要碰撞检测了。Three.js 本身没有完整的物理引擎,而是通过 Raycaster 和 Box3 等工具实现基础碰撞。例如 Box3 可以计算两个物体的包围盒是否相交。更复杂的物理效果(重力、弹跳、关节)则要集成第三方物理引擎(如 Cannon.js 或 Ammo.js)。碰撞检测的工作原理是:先计算两个物体表面的最近点,再判断是否穿透。

1. 构建射线:从鼠标屏幕坐标转换到世界坐标系,再向场景中发射射线。

r=raycast(mouseNDC,camera)\mathbf{r} = \text{raycast}(\text{mouse}_{\text{NDC}}, \text{camera})

2. 检测相交:射线与每个物体的三角形面进行相交测试,找最近的交点。

hit=arg⁡min⁡t  intersect(r,mesh)\text{hit} = \underset{t}{\arg\min} \; \text{intersect}(\mathbf{r}, \text{mesh})

3. 包围盒碰撞:比较两个物体包围盒各轴的范围,若全部重叠则发生碰撞。

AABBA∩AABBB=∅\text{AABB}_A \cap \text{AABB}_B = \emptyset

交互系统是 3D 应用与用户之间的“桥梁”。没有它,Three.js 只是一个“展示盒”;有了它,就变成了“游乐场”。

javascript1 行
const controls = new THREE.OrbitControls(camera, renderer.domElement);

性能优化策略

积木搭得越来越多,场景越来越丰富,但问题也随之而来:画面开始卡顿。就像积木搭得太高,重心不稳,稍微一碰就塌。在 3D 应用里,“卡顿”的本质是帧率(FPS)下降——每秒钟能渲染的画面帧数不足。Three.js 应用的性能优化,就是在和 GPU 的交道中做一个精明的房东,让每一分资源都用在刀刃上。

优化的核心思路有三个方向。第一个方向是减少绘制调用(Draw Calls)。每次 renderer.render() 时,WebGL 需要逐物体进行绘制,一次绘制调用一个物体。如果一个场景里有 1000 个物体,就需要 1000 次绘制调用。优化方法是使用 InstancedMesh 或 mergeBufferGeometries,把同种几何体合并成一个整体。就像把所有相同的积木块粘在一起,变成一个“超大积木”,只需一次绘制。

第二个方向是纹理优化。纹理贴图是显存的大户。5000×5000 的纹理比 1024×1024 的大 25 倍。优化方法包括:压缩纹理格式、动态生成 mipmap(多级纹理)、限制纹理尺寸。使用 RGBELoader 等工具时,也要注意场景中同时加载的纹理总量。

第三个方向是 LOD(Level of Detail,细节层级)——这叫按需简化。当积木离你远,你看不清细节时,用它低精度的替代品显示;当积木靠近时,再切换高精度模型。Three.js 的 THREE.LOD 对象可以帮助实现这一机制,它会根据物体与相机的距离,自动选择不同精度的模型显示。

1. 实例化绘制:相同几何体的多次绘制合并为一次,减少状态切换。

DrawCallsinstanced=1\text{DrawCalls}_{\text{instanced}} = 1

2. LOD 选择:根据距离动态切换模型精度。

level={highd<d1mediumd1≤d<d2lowd≥d2\text{level} = \begin{cases} \text{high} & d < d_1 \\ \text{medium} & d_1 \le d < d_2 \\ \text{low} & d \ge d_2 \end{cases}

3. 纹理压缩:通过压缩格式降低显存占用。

Memory=w×h×bits8\text{Memory} = w \times h \times \frac{\text{bits}}{8}

性能优化是 3D 应用从“能跑”到“流畅跑”的分水岭。记住三把钥匙:减少绘制调用、精简纹理、按需分配细节。就像整理积木盒——相同颜色放一起、大块头放底层、不常用的小件收进抽屉,搭起来自然顺手流畅。

AI 应用场景

智能家居 3D 可视化面板

阶段:数据可视化与交互

解决的问题:传统家居控制面板以二维列表展示设备状态,用户难以直观感知空间布局。使用 Three.js 搭建 3D 户型图,将灯光、窗帘、空调等设备以 3D 模型实时渲染,用户一眼看清全屋状态,点击设备即可控制,极大提升交互效率。

具体案例:某智能家居公司使用 Three.js 构建了 3D 数字孪生面板。户型图按真实尺寸建模,用户进入后看到客厅、卧室、厨房的 3D 模型。点击卧室灯光模型,弹出亮度滑块;拖动滑块,场景中灯光即时变化。结合 AI 节能算法,系统还能在 3D 模型中用颜色标识不同房间的能耗等级,实现可视化节能管理。

在线 3D 商品展示与定制

阶段:互动式前端展示

解决的问题:电商商品展示以图文为主,用户无法全面了解商品的立体结构和细节。基于 Three.js 的商品 3D 展示页让用户随意旋转、缩放产品,甚至可以实时定制颜色、材质。

具体案例:某定制家具品牌在其官网引入 Three.js 3D 展厅。用户可以选择沙发模型进行 360° 旋转,点击“换面料”按钮切换皮料颜色和材质,所有变换实时渲染。该页面上线后,商品详情页停留时长远超图文页面,极大地增强了购买信心。

Python 演示

python65 行
import asyncioimport jsonfrom pyppeteer import launch THREE_JS_APP = """<!DOCTYPE html><html><head><style>body { margin: 0; overflow: hidden; }</style></head><body><script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script><script>  var scene = new THREE.Scene();  var camera = new THREE.PerspectiveCamera(75,    window.innerWidth / window.innerHeight, 0.1, 1000);  var renderer = new THREE.WebGLRenderer();  renderer.setSize(window.innerWidth, window.innerHeight);  document.body.appendChild(renderer.domElement);   var geometry = new THREE.BoxGeometry(1, 1, 1);  var material = new THREE.MeshStandardMaterial({ color: 0x818CF8 });  var cube = new THREE.Mesh(geometry, material);  scene.add(cube);   var light = new THREE.DirectionalLight(0xffffff, 1);  light.position.set(5, 10, 7);  scene.add(light);  scene.add(new THREE.AmbientLight(0x404040));   camera.position.z = 3;   var frames = 0;  function render() {    requestAnimationFrame(render);    cube.rotation.x += 0.01;    cube.rotation.y += 0.01;    renderer.render(scene, camera);    frames++;  }  render();</script></body></html>""" async def main():    browser = await launch()    page = await browser.newPage()    page.on('console', lambda msg: print('[browser]', msg.text))    await page.goto('data:text/html,' + THREE_JS_APP)    await page.waitFor(1000)    perf = await page.evaluate('''() => ({      webgl: !!document.querySelector('canvas'),      size: {        w: renderer.domElement.width,        h: renderer.domElement.height      }    })''')    print('perf:', json.dumps(perf))    await page.screenshot({'path': 'threejs_demo.png'})    await browser.close() asyncio.get_event_loop().run_until_complete(main())
目的:自动化启动一个 Three.js 场景,验证 WebGL 渲染器是否正常创建、画布尺寸是否正确,并输出渲染性能日志。
  1. from pyppeteer import launch引入无头浏览器控制库,相当于让 Python 开一个“隐形浏览器”。
  2. renderer.domElement.width访问 Three.js 渲染器的画布宽度,验证是否按预期尺寸渲染。
  3. page.evaluate()在页面上下文中执行 JavaScript 代码,把 Three.js 关键状态回传给 Python。
  4. page.waitFor(1000)等待 1 秒让首帧渲染完成,模拟真实用户的等待体验。
  5. async def main()使用异步语法管理浏览器生命周期,避免多标签页导致的阻塞。
  6. page.screenshot()截取 3D 场景渲染结果,用于视觉回归测试。
  7. browser.close()关闭浏览器,释放内存资源。

常见误区

误区我把立方体创建出来了,怎么屏幕上什么都没有?是不是我的电脑不支持 WebGL?我明明看到别人的代码也是这样写的啊。

正解最常见的黑屏原因之一就是忘了把物体添加到场景里。就像你搭好了一块积木却放在桌子底下,摄像机(眼睛)自然看不到它。请一定记得 scene.add(mesh),并确认相机的位置正对着物体。此外还应检查相机的 near 和 far 裁剪面,物体如果在裁剪范围之外,也会被悄然隐藏。

为何常见:初学者容易把“创建对象”和“在场景中显示”划等号。而 Three.js 中所有对象都要挂到场景树中才会被渲染,这种“依赖层级”的思维需要刻意练习。

误区我设置的相机参数明明没问题,为什么物体被拉伸了?是不是 Three.js 的 bug?看方形的积木都变成扁的了。

正解这通常是 camera.aspect 没有跟随窗口大小更新。相机的 aspect 代表画面的宽高比,如果创建时是 16:9,而实际窗口变成了 1:1,画面就会明显变形。正确做法是在窗口 resize 时同时更新 camera.aspect = window.innerWidth / window.innerHeight 和 renderer.setSize。

为何常见:人们容易忘记“相机参数是持久状态”——它不会自动感知浏览器窗口变化。很多刚入门的朋友会忽略 window.addEventListener('resize', ...) 这个监听器。

误区既然要不断刷新,直接用 `setInterval(render, 1000/60)` 不就行了吗?反正都是每秒 60 次调用函数。

正解两者有本质区别。requestAnimationFrame 是浏览器原生优化的帧回调机制,其执行频率与屏幕刷新率完全同步(通常是 60Hz),且在页面不可见时会自动暂停,节省资源。而 setInterval 无论页面是否可见都会执行,在后台标签页中会不断积压任务,造成性能浪费;同时在刷新率非 60Hz 的屏幕上也会产生掉帧或撕裂。

为何常见:习惯了传统 JavaScript 定时器逻辑的人,容易用 setInterval 的思维套用到 Three.js 动画上,忽略了渲染循环需要与屏幕刷新同步这一特性。

误区我场景里倒了 1000 个立方体,帧率一下降到 15fps。我的电脑是不是太旧了?需要换显卡才行吗?

正解在 Three.js 中,性能瓶颈往往不是顶点数量,而是绘制调用次数。1000 个独立 Mesh 就是 1000 次绘制调用,这会导致 CPU 和 GPU 之间频繁通信。优化方法是用 InstancedMesh(实例化网格),把 1000 个立方体合并为一次绘制调用。正确的思路不是换硬件,而是用正确的 API 组合。

为何常见:人们习惯用“物体多”来解释卡顿,却没有意识到 WebGL 绘制的瓶颈模型。理解“绘制调用”和“顶点数”的区别,是 3D 性能优化入门的第一课。

误区我的场景好黑,我把 `AmbientLight` 亮度加了好几倍还是没用,是不是 Three.js 的灯光有缺陷?

正解如果物体使用 MeshStandardMaterial 或 MeshPhongMaterial,其亮度依赖于法线方向和光源方向的夹角。若物体表面为纯黑、法线缺失,即使灯光再亮也无法显现。另一种可能是材质被替换成了 MeshBasicMaterial,它完全不响应光照。请先确认材质类型,再调整灯光功率和颜色。

为何常见:初学者通常对材质和灯光的关系认识模糊,不理解“材质决定如何响应光”。从“加灯”到“理解渲染方程”,是入门到进阶的必由之路。

面试题

Q1. Three.js 中,Scene 对象的角色相当于什么?

单选题 · easy

  • A.一个充满魔法的大杯子
  • B.一个存放所有对象的容器← 正确答案
  • C.一个专门输出文字的终端
  • D.一个直接渲染画面的显卡

答案:B。场景是 Three.js 所有对象的容器,相当于搭积木时的底板。选项 A 太玄、C 是控制台的角色、D 是渲染器的功能,只有 B 准确描述了 Scene 的职责。

Q2. 下面哪种相机适合用于模拟人眼的近大远小效果?

单选题 · easy

  • A.透视相机 PerspectiveCamera← 正确答案
  • B.正交相机 OrthographicCamera
  • C.立方相机 CubeCamera
  • D.不可见相机 StereoCamera

答案:A。透视相机的视锥体模拟人眼视觉,具有近大远小的效果,适合 3D 游戏和产品展示。正交相机没有透视变形,适合绘制平面图。C 和 D 是特殊场景才会用到的相机类型。

Q3. Three.js 中,renderer.render(scene, camera) 的作用是?

单选题 · mid

  • A.把场景和相机对象拼接成一个新物体
  • B.根据相机视角将场景绘制到画布上← 正确答案
  • C.把场景中的物体全部删除重画
  • D.将相机的位置移动到一个新坐标

答案:B。renderer.render() 是渲染管线的核心,它使用相机定义的视角来绘制场景中的对象,并输出到画布。它不是拼接物体也不是删除物体,更不是移动相机,而是“拍照”这一动作本身。

Q4. Raycaster 在 Three.js 中的主要用途是什么?

单选题 · mid

  • A.在场景中生成光线的动画效果
  • B.发射虚拟射线检测空间中的对象← 正确答案
  • C.给物体添加材质贴图
  • D.压缩场景中的几何体内存

答案:B。Raycaster 从一个点发射射线,检测射线穿过的所有物体,常用于鼠标点击选中物体。它不生成可见光线、不与材质相关、更不负责内存压缩。在三方场景的交互中,它是“拾取”操作的核心工具。

Q5. 以下哪些方法可以提升 Three.js 应用的渲染性能?

多选题 · mid

  • A.使用 InstancedMesh 合并相同几何体← 正确答案
  • B.使用 LOD 按距离切换模型精度← 正确答案
  • C.在每帧中调用 renderer.setSize 来适配窗口
  • D.提前压缩纹理等资源← 正确答案

答案:ABD。实例化合并减少绘制调用、LOD 按需简化远距离模型、压缩纹理减少显存占用,都是参数化且能大幅提升性能的做法。而每帧调用 setSize 会反复触发渲染尺寸配置,造成性能浪费,应该仅在窗口 resize 时调用。

Q6. MeshStandardMaterial 与 MeshBasicMaterial 最根本的区别在于?

单选题 · mid

  • A.前者颜色多彩,后者只有黑色
  • B.前者受光照影响,后者不受光照影响← 正确答案
  • C.前者只能用于平面,后者只能用于球体
  • D.前者体积大,后者体积小

答案:B。MeshStandardMaterial 基于 PBR 物理渲染模型,需要计算光线和材质交互,而 MeshBasicMaterial 是纯色渲染,不受任何光源影响。这是两者最本质的差异,也决定了它们的适用场景。

Q7. 关于 PerspectiveCamera 的 near 和 far 参数,下列说法正确的是?

多选题 · hard

  • A.它们定义了相机可见的距离范围← 正确答案
  • B.超出 far 的物体将不会被渲染← 正确答案
  • C.near 值越小就能看到越清晰的物体
  • D.两者之比过大可能导致深度缓冲精度不足← 正确答案

答案:ABD。near/far 定义视锥体的裁剪范围,超出部分不会被渲染。near 太小不会提升清晰度,反而可能因深度缓冲区精度问题导致渲染错误。far/near 比值越大,深度精度被稀释的程度越高,这就是选项 D 正确的原因。

Q8. 在 Three.js 动画循环中,使用 Clock.getDelta() 的主要目的是:

单选题 · hard

  • A.计算动画已经运行的总时长
  • B.获取每帧之间的时间间隔以保证帧率无关的速度← 正确答案
  • C.阻塞主线程降低渲染频率
  • D.强制动画帧率固定为 60FPS

答案:B。getDelta() 返回与上一帧的时间差,用来计算“每单位时间”的运动量,保证动画在不同刷新率设备上速度一致。它不计算总时长,更不会阻塞线程或强制帧率。

Q9. OrbitControls 主要让用户能够完成什么操作?

单选题 · mid

  • A.让物体自动旋转
  • B.通过鼠标拖拽旋转、缩放、平移视角← 正确答案
  • C.编辑物体的顶点数据
  • D.生成动态光照贴图

答案:B。OrbitControls 是相机控制器,让用户像操作云台一样控制相机的观察角度。它不旋转物体本身,也不编辑几何体或生成贴图。它是 Three.js 中实现“环绕观察”交互的标准方案。

Q10. 下列哪个是 Three.js 中最基本的可渲染对象?

单选题 · easy

  • A.一个用于存储数据的 JSON 对象
  • B.由几何体和材质组成的 Mesh← 正确答案
  • C.一个用于加载纹理的图片按钮
  • D.一个输出日志到控制台的函数

答案:B。Mesh(网格)是 Three.js 中最基本的渲染对象,它把几何体形状和材质外观组合成一个可渲染的整体。它不是数据存储结构、不是 UI 元素,更不是函数,而是场景中“看得见摸得着”的实体。