ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从 Blender Cycles 到 Three.js:一个校园场景的 Web 渲染排障实录

从 Blender Cycles 到 Three.js:一个校园场景的 Web 渲染排障实录 一个在 Blender 中看起来很漂亮的校园场景导出 GLB 放进浏览器后为什么会变白、变灰、阴影变浅甚至出现黑色地块和闪烁斜纹本文记录一次完整的排查与修复过程。项目背景这次要制作的是一所春季小学的完整校园场景主要包括三层半围合式教学楼面向庭院的开放走廊环形跑道和足球场升旗广场、旗杆和校园道路围墙、大门、路灯、长椅、树木和花坛春季草地及砖墙、道路、建筑外墙等 PBR 材质。模型首先在 Blender 中完成再导出为 GLB最后用 Three.js 制作可旋转、缩放和全屏浏览的 Web 查看器。Blender 中使用 Cycles 渲染时阳光、接触阴影和草地质感都比较自然。但同一个场景进入浏览器以后效果出现了明显落差画面一度接近全白随后又变得灰蒙蒙增强阴影后出现大片黑色区域继续调整后教学楼背面和侧面又出现密集斜纹并在旋转镜头时闪烁。这不是一个参数就能解决的问题而是离线光线追踪与实时光栅化之间的一整套差异。基础环境本项目使用的主要环境如下项目配置操作系统WindowsGPUNVIDIA RTX 3060 12GB建模与离线渲染Blender 4.2、CyclesWeb 渲染Three.js r180模型格式GLB / glTF 2.0色调映射AgX实时阴影4096 × 4096 PCF Shadow Map场景规模约 623 个网格Web GLB 大小约 18 MB烘焙阴影贴图4 张 2048 × 2048 PNG材质使用外部图片纹理构建而不是只依赖 Blender 内置程序节点。这样导出 GLB 后基础颜色、法线和粗糙度等 PBR 信息可以被其他软件和浏览器继续使用。需要注意的是材质贴图可以被导出但 Cycles 的完整渲染效果不会被装进 GLB。Blender 和浏览器之间到底差了什么Cycles 是离线光线追踪渲染器。它会计算光线反弹、间接光、接触阴影、软阴影、环境遮蔽、降噪和颜色管理。Three.js 默认采用实时光栅化渲染。它需要在每一帧内完成绘制主要依赖直接光、环境光、Shadow Map 和屏幕空间效果。即使模型和贴图完全相同两边也不会自然得到一致的画面。可以简单理解为Blender Cycles 模型 PBR 材质 多次光线反弹 接触阴影 降噪 AgX Three.js 模型 PBR 材质 实时灯光 Shadow Map 色调映射因此这次工作的目标不是把 Cycles “原封不动塞进 GLB”而是在浏览器性能允许的范围内重建最重要的视觉关系。问题一模型进入网页后几乎全白现象模型能够正常加载几何结构也完整但建筑、草地和道路整体发白纹理层次很弱。原因最初同时存在多种光照来源GLB 中携带的 Blender 灯光网页中重新创建的太阳光半球环境光环境贴图反射较高的曝光。这些光源叠加后材质被过度照亮。白色建筑首先失去高光层次草地和道路随后也被环境光冲淡。解决加载 GLB 后禁用模型中携带的灯光只保留网页统一管理的太阳与环境光gltf.scene.traverse((obj){if(obj.isLight){obj.visiblefalse;obj.intensity0;}});同时启用 AgX并把默认曝光设为0.95renderer.outputColorSpaceTHREE.SRGBColorSpace;renderer.toneMappingTHREE.AgXToneMapping;renderer.toneMappingExposure0.95;AgX 可以更平稳地压缩高亮区域让白墙和浅色屋顶保留层次而不是直接变成一片纯白。问题二颜色恢复了但画面灰蒙蒙现象降低曝光后模型不再全白但整体颜色偏灰。草地不够绿跑道不够鲜艳画面与 Cycles 相比缺少春天的通透感。原因曝光控制的是整体亮度不等于色彩强度。仅仅降低曝光会同时压暗高光和中间调却不会自动恢复被环境光稀释的色彩。另外浏览器实时渲染和 Cycles 的颜色管理、间接光计算不同即使都使用 AgX也不会像素级一致。解决曝光与饱和度分开控制。最终默认值为曝光0.95 饱和度1.55饱和度作为显示阶段的艺术化校正用来恢复草地、跑道和蓝色屋顶的视觉强度。它不能替代正确的 PBR 材质和灯光只用于补偿浏览器最终画面的色彩差异。问题三阴影总像一层浅灰现象太阳阴影已经开启树木和教学楼也能投影但阴影不够厚重尤其从校园远景观察时建筑仿佛没有真正落在地面上。原因实时阴影主要阻止太阳直射但以下光照仍会继续照亮背光区域半球光环境贴图材质的环境反射PBR 间接填充。所以把 Shadow Map 强度调到1.0只代表太阳直射被完全遮挡并不代表阴影区域会变黑。这也是为什么不断增加太阳强度反而会让受光区域过曝却无法真正解决阴影发浅。解决阴影深度控件同时调整三个部分太阳阴影强度半球补光强度普通非金属材质的环境反射强度。核心逻辑如下functionapplyShadowDepth(rawValue){constvalueTHREE.MathUtils.clamp(Number(rawValue),0,2);constnormalizedMath.min(value,1);sun.shadow.intensityvalue;hemi.intensity0.18*(1-normalized);for(constmaterialofmodelMaterials){material.envMapIntensitymaterial.metalness0.15?(value1?0.10:0.10(1-value)*0.12):(value1?0.0:(1-value)*0.30);}}这样做的重点不是“把黑色画上去”而是减少那些不应该继续照亮阴影的补光来源。问题四增强阴影后出现大片黑色地块最初方案为了接近 Cycles 的接触阴影我们在 Blender 中为以下四个主要接收面烘焙了 SHADOW 贴图校园草地庭院铺装运动跑道升旗广场。最初的 Web 实现是复制一份地面网格在上方覆盖一层透明黑色 ShaderMaterial再按照烘焙图控制透明度。现象阴影确实变深了但足球场、花坛、步道和庭院中出现了大片黑色矩形有些区域还会随着镜头变化而改变。真正原因问题不在烘焙贴图而在“额外透明几何层”本身透明对象通常按照整个对象的距离排序而不是完整的逐像素排序阴影层与原地面几乎处于同一位置会发生 Z-fighting为防止闪烁而设置polygonOffset可能把整张阴影层拉到上方物体之前轻微抬高阴影层只能改变冲突位置不能消除排序问题。因此继续调整透明度、偏移量或覆盖层高度都只是治标。最终解决方案彻底删除额外的阴影网格通过onBeforeCompile把 Cycles 烘焙阴影直接注入地面自身的 MeshStandardMaterialmaterial.onBeforeCompile(shader){shader.uniforms.shadowTextureuniforms.shadowTexture;shader.uniforms.shadowStrengthuniforms.shadowStrength;shader.vertexShadershader.vertexShader.replace(#include common,#include common\nattribute vec2 uv1;\nvarying vec2 vCyclesShadowUv;).replace(#include uv_vertex,#include uv_vertex\nvCyclesShadowUv uv1;);shader.fragmentShadershader.fragmentShader.replace(#include common,#include common\nuniform sampler2D shadowTexture;\nuniform float shadowStrength;\nvarying vec2 vCyclesShadowUv;).replace(#include opaque_fragment,#include opaque_fragment float bakedLight texture2D(shadowTexture, vCyclesShadowUv).r; float bakedFactor mix( 1.0, clamp(bakedLight, 0.035, 1.0), clamp(shadowStrength, 0.0, 1.0) ); gl_FragColor.rgb * bakedFactor;);};现在烘焙阴影只会改变接收面自身的 PBR 像素。足球场、花坛和建筑等上层物体继续使用正常深度缓冲不可能再被透明阴影层穿透。问题五建筑背面出现斜纹旋转镜头时还会闪现象黑色地块消失后教学楼背面和侧面仍然存在密集的点状、斜线状花纹。镜头静止时像材质噪点旋转时则明显闪动。排除模型问题首先检查 Blender 源文件中的教学楼结构。三翼教学楼分别由独立的楼层盒体组成没有重复墙壳也没有两套墙面叠在同一位置。因此这不是模型重面而是典型的 Shadow Acne也就是实时阴影的自遮挡误差。Shadow Acne 为什么会发生方向光使用一张深度贴图记录场景中距离光源最近的表面。渲染墙面时再把当前表面的深度与阴影贴图比较。当阴影图需要覆盖整个校园时4096 × 4096 的像素会被摊到很大的实际范围。墙面实际深度与阴影图中记录的深度非常接近浮点误差就可能让墙面认为自己被自己挡住。这些错误通常呈现为规则斜纹点状噪声与光照方向有关的条带镜头移动时闪烁。第一层修复调整阴影采样最初使用BasicShadowMap阴影边缘较硬但采样格非常明显。最终改成 4KPCFShadowMap并加入小幅正 bias 与 normalBiasrenderer.shadowMap.typeTHREE.PCFShadowMap;sun.shadow.mapSize.set(4096,4096);sun.shadow.bias0.00012;sun.shadow.normalBias0.028;sun.shadow.radius1.25;PCF 会过滤相邻采样减少旋转时的格点跳动正 bias 和法线偏移则把阴影比较位置稍微推离墙面降低自遮挡。为什么不能无限增加 bias过大的 bias 会造成 Peter Panning物体与影子之间出现空隙。树木像悬浮在草地上旗杆的影子也不再从底部开始。因此单纯继续提高全局偏移并不是正确答案。最终修复按对象分配阴影职责教学楼是大型静态盒体最容易出现自阴影误差。最终让教学楼继续投射阴影但不再接收同一张实时太阳阴影图constteachingBuildingName/^(Rear_|Left_|Right_|Side_|Back_|Outer_|Front_|Entrance_|Building_|Canopy_|Clock_|School_)/;obj.castShadowtrue;obj.receiveShadow!teachingBuildingName.test(obj.name);这并不意味着教学楼变成“无光照模型”。墙面的受光与背光仍然由 MeshStandardMaterial、表面法线和太阳方向计算。关闭的只是容易产生误差的 Shadow Map 接收。最终采用的混合渲染方案经过多轮调整项目最终没有追求“所有东西都用同一种阴影”而是按照对象特点分工对象投射实时阴影接收实时阴影Cycles 烘焙阴影教学楼是否否草地、庭院、跑道、升旗广场否或按需否是树木、旗杆、长椅、围墙是是否足球场、花坛、道路等上层物体是是否整个渲染链路可以概括为Blender 建模与 PBR 材质 ↓ 导出带纹理和第二套 UV 的 GLB ↓ Cycles 烘焙四张静态接触阴影图 ↓ Three.js 实时 PBR AgX ↓ 静态大地面使用烘焙阴影 动态/小型物体使用实时阴影 教学楼只投影不接收自身 Shadow Map最终默认参数曝光0.95 饱和度1.55 阴影深度2.0 太阳强度2.80 Shadow Map4096 × 4096 Shadow 类型PCFShadowMap bias0.00012 normalBias0.028 radius1.25这些数值不是适用于所有场景的“标准答案”。它们与校园尺寸、阴影相机范围、太阳角度、模型单位和镜头距离密切相关。真正可复用的是排查思路而不是机械复制参数。这次排障得到的经验1. GLB 保存的是资产不是 Cycles 最终画面GLB 擅长携带模型、UV、图片纹理和 PBR 参数但不会自动保存 Cycles 的光线反弹、降噪和完整光照结果。2. 阴影浅不一定是 Shadow Map 强度不够先检查环境光、环境反射和补光。很多“浅阴影”其实是阴影区域仍被其他光源照亮。3. 不要用重合透明网格模拟大型地面阴影透明排序、深度冲突和 polygon offset 会让问题在不同镜头下反复出现。静态阴影更适合直接进入接收面的材质。4. 斜纹和旋转闪烁要优先怀疑 Shadow Acne可以临时关闭异常对象的receiveShadow。如果条纹立即消失就应调整 Shadow Map、bias、阴影范围或对象的阴影职责而不是去修改墙面贴图。5. 大场景不适合一个阴影策略管到底建筑、地面、树木和细杆的尺寸差异很大。按对象类型组合实时阴影与烘焙阴影往往比不断堆高全局参数更稳定。结语将 Blender 场景发布到 Web不是简单地“导出一个 GLB 就结束”。建模和材质只是资产阶段浏览器仍需要重新搭建颜色管理、灯光、环境反射和阴影策略。这次校园场景最终采用了实时 PBR、Cycles 烘焙阴影和选择性实时 Shadow Map 的混合方式。它无法与 Cycles 做到像素级一致但在保留旋转、缩放和实时交互的前提下已经获得了稳定、清晰且具有空间重量感的效果。更重要的是我们没有用更多临时偏移去掩盖问题而是分别处理了过曝、环境补光、透明排序、深度冲突和建筑自阴影。对于类似的建筑 Web 可视化项目这套排查路径通常比反复试参数更有价值。
返回列表