three.js CubeCamera 深入解析:用立方体相机实时捕获环境反射的完整实现
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
CubeCamera 是 three.js 中用于「实时环境捕获」的特殊相机:它被放置在 3D 空间中的某个位置,将周围的场景渲染进一个立方体渲染目标(Cube Render Target),再将该目标作为环境贴图(Environment Map)驱动场景中的实时反射。阅读本文后,你将掌握 CubeCamera 的构造参数、update()六面渲染流程、activeMipmapLevel与coordinateSystem两个进阶属性,以及从源码层面理解六台子相机的排布、WebGL/WebGPU 坐标系切换与 PMREM 更新机制。
一、CubeCamera 定位:实时环境反射的核心组件
CubeCamera继承自EventDispatcher → Object3D(见 src/cameras/CubeCamera.js),它本身不是一个独立的可视相机,而是一个「容器」——内部包含 6 台PerspectiveCamera,分别朝 +X、-X、+Y、-Y、+Z、-Z 六个方向观察,把整个 360° 视野拍进WebGLCubeRenderTarget的六个面中。
典型应用场景包括:
- 实时反射材质:铬合金车身、镜面球等把动态场景映射到
envMap上; - 环境贴图动态化:将背景立方体贴图实时重采样,供
PMREMGenerator处理; - LightProbe 生成:先把场景拍成立方体贴图,再用
LightProbeGenerator.fromCubeRenderTarget()转为光照探针。
官方文档代码示例
文档给出的标准用法(来自 src/cameras/CubeCamera.js 的 JSDoc 注释,与官方 API 文档一致):
// 1. 创建立方体渲染目标(开启 mipmap,用于平滑的环境采样) const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 256, { generateMipmaps: true, minFilter: THREE.LinearMipmapLinearFilter } ); // 2. 创建立方体相机(near=1, far=100000) const cubeCamera = new THREE.CubeCamera( 1, 100000, cubeRenderTarget ); scene.add( cubeCamera ); // 3. 创建一辆「镀铬」汽车,把立方体贴图作为环境贴图 const chromeMaterial = new THREE.MeshLambertMaterial( { color: 0xffffff, envMap: cubeRenderTarget.texture } ); const car = new THREE.Mesh( carGeometry, chromeMaterial ); scene.add( car ); // 4. 更新渲染目标:先隐藏汽车自身,再把相机移到汽车位置拍摄 car.visible = false; cubeCamera.position.copy( car.position ); cubeCamera.update( renderer, scene ); // 5. 恢复正常渲染 car.visible = true; renderer.render( scene, camera );这段代码中有三个关键细节:
generateMipmaps: true+LinearMipmapLinearFilter:环境贴图在不同距离/粗糙度下会被以不同 mipmap 级别采样,开启 mipmap 后反射过渡更平滑,这也是示例特意声明这两项配置的原因;car.visible = false:拍摄时必须把「会被反射的物体本身」隐藏,否则它会拍到自己在自己内部的投影,产生错误的自反射;cubeCamera.position.copy( car.position ):相机位置即反射观察点,放在物体中心才能让反射角度与物理镜像一致。
二、构造函数:near、far 与 renderTarget
new CubeCamera( near : number, far : number, renderTarget : WebGLCubeRenderTarget )
| 参数 | 类型 | 说明 |
|---|---|---|
near | number | 六台子相机的近裁剪平面,距离相机小于该值的几何体不会被拍入立方体贴图 |
far | number | 六台子相机的远裁剪平面,决定反射「能看多远」,示例中常用1000甚至100000以覆盖大场景 |
renderTarget | WebGLCubeRenderTarget | 渲染目标,其.texture即可直接赋给材质的envMap |
从源码看(src/cameras/CubeCamera.js),构造过程做了一件很巧妙的事:
const fov = - 90; // negative fov is not an error const aspect = 1;6 台子相机全部使用负值 fov = -90、aspect = 1构造。负 fov 在 three.js 中是合法的:它表示视角方向被反转,配合子相机朝六个轴向的lookAt,使每台相机正好覆盖该轴向的一个 90° 视锥,六台拼起来恰好无缝铺满整个立方体内部视野,且各面之间不重叠、不遗漏。
const cameraPX = new PerspectiveCamera( fov, aspect, near, far ); cameraPX.layers = this.layers; this.add( cameraPX ); // … cameraNX / cameraPY / cameraNY / cameraPZ / cameraNZ 同理注意cameraPX.layers = this.layers:每台子相机的图层直接引用CubeCamera 的layers对象。这意味着通过cubeCamera.layers.enable( n )控制可见图层时,六台子相机会同步生效——这正是官方示例中实现「选择性捕获」(比如只拍部分物体做镜像效果)的底层支撑。
三、属性详解
3.1.activeMipmapLevel : number(默认0)
当前激活的 mipmap 级别。update()内部会将renderer.setRenderTarget( renderTarget, face, activeMipmapLevel )的第三个参数设为该值,从而决定本次渲染写入哪个 mipmap 层。
仓库中的实战用例在 examples/webgl_materials_cubemap_render_to_mipmaps.html:它把一张高分辨率立方体贴图逐层手动降采样,循环里同时调整 viewport 与 mipmap 级别:
const cubeCamera = new THREE.CubeCamera( 1, 10, cubeMapRenderTarget ); for ( let mipmap = 0; mipmap < mipmapCount; mipmap ++ ) { material.uniforms.mipIndex.value = mipmap; material.needsUpdate = true; // 缩小视口到 1/2^mipmap cubeMapRenderTarget.viewport.set( 0, 0, cubeMapRenderTarget.width >> mipmap, cubeMapRenderTarget.height >> mipmap ); cubeCamera.activeMipmapLevel = mipmap; // 关键:写入目标 mipmap 层 cubeCamera.update( renderer, mesh ); }这是activeMipmapLevel的典型用法:在硬件 mipmap 生成不可靠或需要自定义降采样过滤(如 CubemapFilterShader)时,用 CubeCamera 逐层重采样整个立方体。
3.2.coordinateSystem : WebGLCoordinateSystem | WebGPUCoordinateSystem(默认null)
当前激活的坐标系约定。由于 WebGL 与 WebGPU 在剪贴空间 Z 方向上约定不同(z ∈ [0, 1]vsz ∈ [-1, 1]),CubeCamera 需要知道自己在哪个渲染后端下工作,才能给六台子相机设置正确的朝向。该属性默认null,首次update()时会自动同步为renderer.coordinateSystem,之后若切换渲染器(例如从 WebGLRenderer 换到 WebGPURenderer)会自动检测到差异并重新调用updateCoordinateSystem()修正朝向。
3.3.renderTarget : WebGLCubeRenderTarget
对立方体渲染目标的引用。常用派生操作:
- 读取贴图:
cubeCamera.renderTarget.texture(赋给material.envMap或scene.background); - 尺寸:
renderTarget.width / height(构造时传入的size,如new WebGLCubeRenderTarget( 256 )即为 256×256 每面); - 清理:
renderTarget.clear( renderer )会逐面清空颜色、深度与模板缓冲(src/renderers/WebGLCubeRenderTarget.js)。
WebGLCubeRenderTarget本身继承自WebGLRenderTarget,构造时会创建一个带isRenderTargetTexture = true标记的CubeTexture(src/renderers/WebGLCubeRenderTarget.js)。源码注释解释了这一标记的由来:按 RenderMan 传统约定,立方体贴图使用左手系描述 px/nx 面,而 three.js 场景是右手系,isRenderTargetTexture让渲染器在采样时自动完成 px/nx 的交换,保证外部加载的旧式立方体贴图与实时渲染的立方体贴图都能正确显示。
四、方法深度解析
4.1.update( renderer, scene )
用给定渲染器把场景渲染进立方体渲染目标。参数:
- renderer:
Renderer | WebGLRenderer; - scene:要拍摄的
Scene。
从源码实现看(src/cameras/CubeCamera.js),update()内部流程可拆解为五步:
update( renderer, scene ) { if ( this.parent === null ) this.updateMatrixWorld(); // ① 未入场景时手动刷新矩阵 const { renderTarget, activeMipmapLevel } = this; if ( this.coordinateSystem !== renderer.coordinateSystem ) { this.coordinateSystem = renderer.coordinateSystem; this.updateCoordinateSystem(); // ② 坐标系变更时重排子相机 } const [ cameraPX, cameraNX, cameraPY, cameraNY, cameraPZ, cameraNZ ] = this.children; // ③ 保存现场并做环境快照 const currentRenderTarget = renderer.getRenderTarget(); const currentActiveCubeFace = renderer.getActiveCubeFace(); const currentActiveMipmapLevel = renderer.getActiveMipmapLevel(); const currentXrEnabled = renderer.xr.enabled; renderer.xr.enabled = false; // 关闭 XR,避免立体渲染干扰六面拍摄 const generateMipmaps = renderTarget.texture.generateMipmaps; renderTarget.texture.generateMipmaps = false; // ④ 前 5 面禁用 mipmap 生成 // ⑤ 依次渲染 6 个面(face 0~4) renderer.setRenderTarget( renderTarget, 0, activeMipmapLevel ); renderer.render( scene, cameraPX ); // … face 1/2/3/4 对应 NX / PY / NY / PZ // 最后一面恢复 mipmap 开关:mipmap 在最后一次 render() 调用时生成 renderTarget.texture.generateMipmaps = generateMipmaps; renderer.setRenderTarget( renderTarget, 5, activeMipmapLevel ); renderer.render( scene, cameraNZ ); // 恢复渲染器状态 renderer.setRenderTarget( currentRenderTarget, currentActiveCubeFace, currentActiveMipmapLevel ); renderer.xr.enabled = currentXrEnabled; renderTarget.texture.needsPMREMUpdate = true; // 通知 PMREM 需要重新生成 }其中值得展开的几个机制:
mipmap 延迟生成:
update()会临时把renderTarget.texture.generateMipmaps置为false,前 5 个面渲染完后再恢复。源码注释说明了原因:mipmap 是在最后一次render()调用时统一生成的,此时立方体的六个面都已就绪,mipmap 链才完整。若六面都开着自动生成,中间面会基于尚未渲染完成的数据计算 mipmap。反转深度缓冲(reversed depth buffer)兼容:若渲染器处于反转深度模式且
autoClear === false,每个面渲染前会先clearDepth(),避免上一次面的深度数据污染当前面。状态保存/恢复:
update()是「有副作用的调用」——它会覆盖当前渲染目标与 XR 状态,并在结束前完整恢复。因此它可以安全地嵌在任何渲染循环中(例如先cubeCamera.update()再renderer.render( scene, camera ))。needsPMREMUpdate = true:每次更新后都会触发。对应 src/textures/Texture.js 中的 setter:set needsPMREMUpdate( value ) { if ( value === true ) { this.pmremVersion ++; // 版本号递增,PMREMGenerator 据此判断需要重新处理 } }这解释了为什么使用
PMREMGenerator消费 CubeCamera 结果时,每次场景变化后 PMREM 都会自动重新生成——机制靠的就是这个版本号。
此外,WebGLCubeRenderTarget.fromEquirectangularTexture()内部同样复用了 CubeCamera(src/renderers/WebGLCubeRenderTarget.js):用一个CubeCamera( 1, 10, this )拍摄一个背面着色、按等距圆柱坐标采样片元的盒子,从而把等距圆柱全景图转换为立方体贴图。这印证了 CubeCamera 在 three.js 内部是「一切立方体捕获」的通用基础设施。
4.2.updateCoordinateSystem()
当 CubeCamera 的坐标系约定发生变化时必须手动调用。它根据this.coordinateSystem的值重排六台子相机的up与lookAt方向(src/cameras/CubeCamera.js):
WebGLCoordinateSystem:cameraPX.up.set( 0, 1, 0 ); cameraPX.lookAt( 1, 0, 0 ); cameraNX.up.set( 0, 1, 0 ); cameraNX.lookAt( - 1, 0, 0 ); cameraPY.up.set( 0, 0, - 1 ); cameraPY.lookAt( 0, 1, 0 ); cameraNY.up.set( 0, 0, 1 ); cameraNY.lookAt( 0, - 1, 0 ); cameraPZ.up.set( 0, 1, 0 ); cameraPZ.lookAt( 0, 0, 1 ); cameraNZ.up.set( 0, 1, 0 ); cameraNZ.lookAt( 0, 0, - 1 );WebGPUCoordinateSystem:X 面朝向对调(PX 看 -1,0,0)、所有up相应翻转,以适配 WebGPU 的剪贴空间与 Y 轴约定。其他值直接抛出
Invalid coordinate system错误。
由于update()已经内置了坐标系一致性检查与自动重排,绝大多数场景下不需要显式调用updateCoordinateSystem();手动调用主要出现在「先设定coordinateSystem再渲染」的初始化序列或跨渲染器迁移场景中。
五、典型实战模式
5.1 动态环境反射(每帧或按需更新)
仓库示例 examples/webgl_materials_cubemap_dynamic.html 展示了最小化的动态反射流程:创建WebGLCubeRenderTarget( 256 )与CubeCamera( 1, 1000, cubeRenderTarget ),在controls的change回调里调用cubeCamera.update( renderer, scene ),把cubeRenderTarget.texture挂到材质的envMap上。注意其 near 取1而非文档示例的1、far 取1000——near/far 的具体数值应贴合场景尺度,过小会丢远处反射,过大则压缩深度精度。
5.2 从立方体贴图生成 LightProbe
examples/webgl_lightprobe_cubecamera.html 是一条完整的「几何→光照」管线:
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 256 ); cubeCamera = new THREE.CubeCamera( 1, 1000, cubeRenderTarget ); // … cubeCamera.update( renderer, scene ); // 先把场景(含天空盒背景)拍成立方体贴图 const probe = await LightProbeGenerator.fromCubeRenderTarget( renderer, cubeRenderTarget ); lightProbe.copy( probe ); scene.add( new LightProbeHelper( lightProbe, 5 ) );这里 CubeCamera 承担的是「离屏环境采样」角色:把当前场景的漫反射信息固化为 256×256 的立方体贴图,再交由LightProbeGenerator分解成球谐系数(SH),最终让 PBR 材质获得基于几何的间接光照。同目录下的 WebGPU 版本 examples/webgpu_lightprobe_cubecamera.html 与 examples/webgpu_cubemap_dynamic.html 验证了同一套 API 在 WebGPU 渲染器下的等价性——update()内部会根据renderer.coordinateSystem自动切换到 WebGPU 朝向,业务代码无需改动。
5.3 镜像/反射球效果
examples/webgl_animation_skinning_ik.html 中用new THREE.CubeCamera( 0.05, 50, cubeRenderTarget )给球体做局部镜像:near 收到0.05,说明镜像观察点紧贴球体表面,far 只需50覆盖局部即可。这个参数组合是「近距离局部反射」的推荐写法,与全景反射(near 1 / far 上千)形成对照。
六、测试与继承验证
单元测试位于 test/unit/src/cameras/CubeCamera.tests.js,验证了三条基本契约:
CubeCamera可实例化;new CubeCamera() instanceof Object3D === true(继承链EventDispatcher → Object3D);object.type === 'CubeCamera'(类型标记,供序列化与渲染器类型分发使用)。
七、实践要点总结
- 参数速查:
new CubeCamera( near, far, renderTarget );near/far 按反射可见范围取,renderTarget 尺寸(128~512)按反射精细度权衡显存与耗时。 - 拍摄时机:在
renderer.render()之前调用cubeCamera.update( renderer, scene );update()会自动保存并恢复渲染器状态,但拍摄对象自身需要手动visible = false。 - mipmap 配置:用于 PBR 环境反射时建议
generateMipmaps: true+LinearMipmapLinearFilter;update()内部对 mipmap 生成时机的处理已经保证了六面完整性。 - 跨渲染器:WebGL 与 WebGPU 渲染器下 API 完全一致,坐标系差异由
coordinateSystem属性与updateCoordinateSystem()自动处理。 - 进阶:
activeMipmapLevel支持手工逐层重采样立方体(见 examples/webgl_materials_cubemap_render_to_mipmaps.html);layers的引用共享支持选择性捕获;needsPMREMUpdate机制保证 PMREM 缓存与实时立方体贴图同步失效。
核心源码与资料索引:
- 实现:src/cameras/CubeCamera.js
- 渲染目标:src/renderers/WebGLCubeRenderTarget.js
- PMREM 失效机制:src/textures/Texture.js
- 单元测试:test/unit/src/cameras/CubeCamera.tests.js
- 实战示例:examples/webgl_lightprobe_cubecamera.html、examples/webgl_materials_cubemap_dynamic.html、examples/webgl_shaders_sky.html
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考