如何在Three.js/R3F中预计算阴影?编辑器场景阴影烘焙方案
基于R3F/Three.js实现静态阴影预烘焙方案
核心实现逻辑(对标Trackmania的预烘焙机制)
Trackmania的核心思路就是把静态场景的光照和阴影提前计算好,存储为纹理文件,运行时直接将纹理贴在物体上,完全跳过实时阴影计算流程。放到R3F/Three.js的场景里,你可以按以下步骤落地:
1. 先区分静态/动态物体
第一步必须在编辑器里给物体加标记,明确哪些是创建后就不会移动、变形的(静态物体),哪些是会动态变化的(动态物体)。阴影烘焙只针对静态物体,动态物体要么用简化版实时阴影,要么用光照探针补充环境光。
2. 用Three.js的Lightmap系统完成烘焙
Three.js自带成熟的光照贴图烘焙工具,具体操作流程:
- 给每个静态物体的材质分配空白的
lightMap纹理,纹理尺寸按需设置:大物体用1024x1024,小物体用256x256即可,避免无意义的高分辨率浪费内存 - 配置烘焙专用场景:关闭实时阴影开关,把光源参数调整到和最终展示场景完全一致(否则烘焙出的光影会和实际展示效果脱节)
- 调用
renderer.generateLightMap或LightmapGenerator工具,将阴影和光照信息渲染到对应的lightMap纹理上 - 烘焙完成后切换到展示模式,静态物体直接使用烘焙好的lightMap,同时关闭它们的
receiveShadow属性和光源的castShadow属性,彻底禁用实时阴影计算
3. 编辑器端的烘焙流程优化
- 不要单个物体单独烘焙,把相邻的静态物体合并为一组,统一烘焙到同一张光照贴图(类似纹理图集),减少纹理数量和Draw Call
- 烘焙完成后必须对纹理进行压缩,用Basis Universal、ETC2这类浏览器原生支持的压缩格式,显存占用能降到原有的1/4甚至更低
- 光照贴图不需要Alpha通道,直接用RGB8或RGB565格式存储,进一步压缩内存占用
光照贴图方案的可行性(针对大量物体的场景)
这个方案完全可行,但需要做好针对性优化,才能避免内存爆炸和性能下降:
1. 内存控制技巧
- 合并纹理与几何体:将小型静态物体合并为单个BufferGeometry,再统一烘焙到一张光照贴图,避免几百个物体对应几百张纹理的内存浪费
- 分级精度分配:根据物体的大小、视觉优先级分配纹理分辨率——远处的、微小的物体用低分辨率,近处的大物体用高分辨率
- 分块加载:如果场景规模极大,就将场景拆分为若干区块,每个区块单独烘焙,用户浏览到对应区块时再加载该区块的烘焙纹理,不用一次性全塞进内存
2. 展示阶段的性能保障
- 静态物体彻底关闭所有实时阴影相关设置,仅给动态物体开启简化版实时阴影(比如把阴影贴图分辨率降到1024x1024,关闭软阴影效果)
- 合并后的静态物体用单个BufferGeometry渲染,Draw Call能从几百降到个位数,渲染性能会大幅提升
- 优先使用WebGL2的纹理压缩功能,目前主流浏览器均支持,压缩后的纹理几乎不会占用过多显存
补充方案(混合场景适配)
如果场景中动态物体占比高,纯光照贴图方案不够用,可以结合以下两种方式:
- 光照探针(Light Probes):为动态物体提供近似的环境光照,无需实时计算全局光照
- 静态烘焙+动态投影:静态物体的阴影烘焙到光照贴图,动态物体的阴影仅实时投影到静态物体上,同时限制阴影贴图分辨率,平衡视觉效果和性能
简化代码示例(R3F中触发烘焙)
import { useThree } from '@react-three/fiber'; import { LightmapGenerator, WebGLRenderTarget } from 'three'; // 编辑器模式下触发烘焙的函数 const bakeStaticShadows = () => { const { scene, renderer, camera } = useThree(); // 筛选所有标记为静态的网格物体 const staticMeshes = scene.children.filter(obj => obj.userData.isStatic && obj.isMesh); // 给每个静态物体分配光照贴图 staticMeshes.forEach(mesh => { // 根据物体尺寸选择纹理分辨率,示例用512x512 const lightMapTarget = new WebGLRenderTarget(512, 512, { type: THREE.HalfFloatType }); mesh.material.lightMap = lightMapTarget.texture; mesh.material.lightMapIntensity = 1.2; // 可根据场景调整光照强度 }); // 执行光照贴图烘焙 const lightmapGenerator = new LightmapGenerator(scene); lightmapGenerator.computeLightmaps(staticMeshes, camera); // 切换到展示模式,禁用实时阴影 scene.traverse(obj => { if (obj.isLight) obj.castShadow = false; if (obj.userData.isStatic) obj.receiveShadow = false; }); };
内容的提问来源于stack exchange,提问作者delorme victor
相关产品推荐
相关产品推荐

