下载 App
10/16案例分享
【案例分享】渲染参数为什么"配了没反应":LightGroup、Zone、后效这条链路上的 8 个坑
投稿方向:星火编辑器论坛「案例分享」专区 / 生态贡献度
类型:图文案例
素材来源:UrhoX 渲染配置说明 + 三个 3D 场景项目的实测记录
一句话结论
渲染这块最贵的 bug,是画面正常、控制台干净、你配的参数却没进 shader——它不报错,只是被另一个优先级更高的组件顶掉了。
我按"光照 → 雾与天空 → 环境光 → 后效"把这条链路走一遍,每段给能直接粘的 Lua 和具体数值。
一、先搞清楚谁覆盖谁
场景里三层在抢同一份画面控制权:
层:载体 谁决定它生效
光照:LightGroup 节点(内含 Zone + Directional Light) 场景里有没有第二个 Zone
雾 / 环境光:Zone 组件 priority 谁大
后效:还是 Zone 组件上的开关 你拿到的那个 Zone 实例是不是生效的那个
规则一句话:Zone 按 priority 抢,数大的赢;相机位置决定你在哪个 Zone 的 bbox 里。
LightGroup 预设自带的 Zone 是 priority = -1,兜底背景。你随手 CreateComponent("Zone") 出来的是 priority = 0,必然赢。很多人说"配了 IBL 和 Bloom 全没了",其实没丢,是被自己的新 Zone 顶掉了。
Skybox 在 postopaque pass 渲染,不计算雾——雾再浓天空照常可见,这直接决定天空盒该挂哪个 technique。
二、光照:太阳用预设,点光自己建
LightGroup 一次加载就把太阳、雾、IBL、Bloom、SH 全配齐了。
-- 场景光照一次到位
local lg = scene_:CreateChild("LightGroup")
lg:LoadXML(cache:GetResource("XMLFile", "LightGroup/Daytime.xml"):GetRoot())
-- 想微调就递归取组件改,不要新建
local zone = lg:GetComponent("Zone", true) -- 第二个参数 true = 递归查子节点
local light = lg:GetComponent("Light", true)
light.brightness = 3.0
light.color = Color(1, 0.9, 0.8)
light.castShadows = true
预设 7 个,按氛围挑:
文件:光色 brightness
Daytime.xml:暖白 3
Dusk.xml:橙 5
Sandstorm.xml:暖黄 6
Night.xml:冷蓝 0.8
Midnight.xml:冷蓝 1.1
DarkNight.xml:紫蓝 1.3
BloodNight.xml:冷白 0.55
火把、车灯、霓虹、爆炸光闪这类点光和追光,预设不管,必须自己建:
-- 篝火
local n = scene_:CreateChild("Campfire")
n.position = Vector3(5, 0.5, 0)
local p = n:CreateComponent("Light")
p.lightType = LIGHT_POINT
p.color = Color(1.0, 0.6, 0.2)
p.brightness = 80
p.range = 12.0
p.castShadows = false -- 点光阴影要渲染 6 个面,开销大
-- 手电 / 车灯
local s = cameraNode:CreateChild("Flashlight")
s.direction = Vector3(0, 0, 1)
local sp = s:CreateComponent("Light")
sp.lightType = LIGHT_SPOT
sp.color = Color(1, 1, 1)
sp.brightness = 3000
sp.range = 20.0
sp.fov = 45.0 -- 光锥张角(度)
衰减公式和 UE 一致(1/(d²+1) 加窗口函数),数值能直接对标 UE 坎德拉——前提是开了 auto-exposure。表是我对着美术调的:
用途:brightness range(米)
暗房间小蜡烛:1 3–5
室内台灯:30 6–12
单根火把:25–30 5–8
篝火:80 8–15
路灯:500 15–25
车灯 / 手电:3000 20–40
爆炸瞬闪:5000+ 看场景
没开 auto-exposure 时整套值要小两个量级:蜡烛 0.5、台灯 5、篝火 8、路灯 30。硬套大值就是一片白。
三、雾与天空
雾分深度雾(按相机距离)和高度雾(按世界 Y),能同时开:
local zone = lg:GetComponent("Zone", true)
zone.fogColor = Color(0.6, 0.7, 0.9)
zone.fogStart = 50 -- 50 米开始起雾
zone.fogEnd = 300 -- 300 米完全融入雾色
zone.fogDensity = 1.0 -- 1.0 默认;0 = 关深度雾;只影响深度雾
zone.heightFog = true -- 高度雾要显式开
zone.fogHeight = 5.0 -- 雾面高度(世界 Y)
zone.fogHeightScale = 0.5 -- 越大衰减越快
常用档位:晴天 80/300,阴天 30/150,夜晚 20/100,浓雾 5/40,极端贴脸雾 2/15。天空盒统一是 Box.mdl + Techniques/DiffSkybox.xml,三种来源:
-- A. 程序化渐变天空(推荐)
local SkyUtils = require "urhox-libs.Rendering.SkyUtils"
SkyUtils.CreateGradientSky(scene_, {
zenith = Color(0.06, 0.18, 0.52),
horizon = Color(0.28, 0.52, 0.82), -- 这个值跟 fogColor 对齐
ground = Color(0.15, 0.25, 0.35),
skyExp = 0.5,
hdrBoost = 2.0,
})
-- B. 官方预制 cubemap,写实风格 A,最通用
local mat = Material:new()
mat:SetTechnique(0, cache:GetResource("Technique", "Techniques/DiffSkybox.xml"))
mat:SetTexture(TU_DIFFUSE, cache:GetResource("TextureCube", "uuid://E4ehad-5axjv7Ml9zasxNx06"))
local skyNode = scene_:CreateChild("Sky")
local skybox = skyNode:CreateComponent("Skybox")
skybox:SetModel(cache:GetResource("Model", "Models/Box.mdl"))
skybox:SetMaterial(mat)
官方一共给了 9 个预制天空球,写实 4 个(uuid://E4ehad-...、E_LhMeSIwE-...、ENC1IXaQqFYisE64aHW9CwX0、GNxJASZSCM8_0Qv-m3hrFG9c),卡通 5 个。PBR 走写实,低多边形走卡通。
天空偏暗偏灰?ACES 在压它
程序化生成的 cubemap 是 LDR 值(0~1),Skybox shader 内部会走一次 ACES 色调映射,低值被压得更低。修法是把颜色推回 HDR 范围:
mat:SetShaderParameter("MatDiffColor", Variant(Vector4(2.0, 2.0, 2.0, 1.0)))
SkyUtils.CreateGradientSky 的 hdrBoost(默认 2.0)就是干这件事的,自己拼材质时得手动补。
这个 2 倍别无脑套。从文件加载的 DDS cubemap 通常已经是 HDR 值,再加就是过曝。只有程序化生成的 LDR cubemap 才需要。
四、环境光:三种来源,默认那个改不动
Zone 的环境光有三个模式:
模式:行为
AMBIENT_PREBAKED(默认):用预烘焙的 SH 系数 + 镜面 cubemap
AMBIENT_SKYBOX:自动抓场景 Skybox 的 cubemap 烘焙 SH + IBL
AMBIENT_COLOR:平铺纯色,直接喂 shader 的 cAmbientColor,不烘焙
-- 跟天空自动对齐(最省事)
zone.ambientSource = AMBIENT_SKYBOX
zone.ambientIntensity = 1.0
-- 平铺纯色,卡通或快速调试用
zone.ambientSource = AMBIENT_COLOR
zone.ambientColor = Color(0.3, 0.3, 0.35)
sourceTexture 塞 Texture2D(全景图)也行,引擎自动转 cubemap 再烘。
烘焙跑在后台线程,不阻塞渲染。同一张纹理的结果会被缓存,再设一次即时生效,不用重烘。烘焙结果和预烘焙数据分开存,ambientSource 切回 AMBIENT_PREBAKED 就恢复预设值——所以来回切天空不会污染原始数据,可以放心试。
一个数记住:ambientIntensity 默认 0.3,走 AMBIENT_SKYBOX 或 sourceTexture 烘焙时必须推到 1.0,不然整个场景暗一档,你会以为是天空贴图的问题。
五、后效四件套
后效全挂在 Zone 上。
后效:开关 默认
Vignette 暗角:zone.vignetteEnabled OFF
AutoExposure 自动曝光:zone.autoExposureEnabled OFF
GaussianBlur 全屏模糊:SetAttribute("Gaussian Blur Enable", ...) OFF
Tonemap 色调映射:zone.tonemapMode LUT
-- 夜景:自动曝光 + 浓一点的暗角
local lg = scene_:CreateChild("LightGroup")
lg:LoadXML(cache:GetResource("XMLFile", "LightGroup/Night.xml"):GetRoot())
local zone = lg:GetComponent("Zone", true)
zone.autoExposureEnabled = true
zone.vignetteEnabled = true
zone.vignetteIntensity = 2.0 -- 1.0 柔和 / 2–3 较浓 / 4–5 电影感 / 6+ 压黑
-- 暂停时虚化背景
zone:SetAttribute("Gaussian Blur Enable", Variant(true))
tonemap 三个模式:TONEMAP_MODE_NONE = 0,TONEMAP_MODE_ACES = 1,TONEMAP_MODE_LUT = 2。
Vignette 贴图可以换,给一张中心高亮、四角偏暗的灰度 PNG 就行:
zone.vignetteTexture = cache:GetResource("Texture2D", "Textures/MyVignette.png")
LightGroup 预设里已经内置了 vignette 贴图,直接开 vignetteEnabled 就有,不用自备。
六、踩过的坑
坑 1:加载 LightGroup 之后又建了一个 Zone。
-- ❌ 新建的 Zone priority=0 > 预设的 -1,整块 Zone 被替换
local lg = scene_:CreateChild("LightGroup")
lg:LoadXML(f:GetRoot())
local zone = scene_:CreateComponent("Zone") -- 错
zone.fogColor = Color(0.5, 0.6, 0.8)
-- ✅ 取预设里那个 Zone 改
local zone = lg:GetComponent("Zone", true) -- 对
zone.fogColor = Color(0.5, 0.6, 0.8)
IBL、SH 光照、Bloom、雾效、水面反射一起没。静默的,画面还能看,只是"变平了"。
坑 2:天空盒挂 DiffUnlit.xml。
-- ❌ DiffUnlit 是普通几何体 shader,会按距离吃雾
mat:SetTechnique(0, cache:GetResource("Technique", "Techniques/DiffUnlit.xml"))
-- ✅ 天空盒专用,postopaque pass,不算雾
mat:SetTechnique(0, cache:GetResource("Technique", "Techniques/DiffSkybox.xml"))
用错的话,近距浓雾里天空会被染成雾色,彻底看不见。
坑 3:fogColor 挑了饱和的红或粉。
雾不只染天空地平线,所有超过 fogStart 的物体都会被染成 fogColor。我做过一版红色警戒雾,整个地图连近处石头都是粉的。雾色用低饱和自然色:灰蓝、灰白、暖灰。
坑 4:camera.farClip 2000,Zone bbox 只给了 ±500。
每个物体按自身位置找所属 Zone。远处物体跑出你的 bbox,就落到 LightGroup 的低优先级 Zone 上,雾参数换成另一套——表现是远端雾突然变淡或变色。
-- 二选一
zone.boundingBox = BoundingBox(Vector3(-2000,-2000,-2000), Vector3(2000,2000,2000))
zone.override = true -- 或让引擎跳过远端 Zone 查找
不确定世界多大就 ±1000 兜底。
坑 5:TONEMAP_MODE_LUT 在移动端静默走 ACES。
LUT 生效要同时满足两条:桌面端(DESKTOP_GRAPHICS),以及 LUT 资源加载成功。iOS / Android / surfaceless 根本没有 LUT 路径,选了也走 ACES,不报错。Zone 没指定 LUT 时引擎会懒加载内置的 Textures/TonemapLut.cube,裁剪过的部署如果没带这个资源,同样静默回落。
顺带一个连带坑:TONEMAP_MODE_NONE 会让 autoExposureEnabled 一起失效——曝光缩放只在 tonemap 分支里应用,直方图 pass 仍在跑但不影响画面。
坑 6:给 GaussianBlur 找 property。
这个开关只有 attribute,没有 Lua property(其余后效都有)。写 zone.gaussianBlurEnabled = true 是往组件上挂了个没人读的字段,画面一点不动。
zone:SetAttribute("Gaussian Blur Enable", Variant(true))
坑 7:以为改个 zone.ambientColor 就能提亮暗部。
默认的 AMBIENT_PREBAKED 模式下这行是 no-op。引擎在 shader 里硬下发 cAmbientColor=(0,0,0,0),环境光完全由 SH 球谐驱动。我在这上面耗了半下午,反复调色值画面纹丝不动。想要平铺纯色得先切 AMBIENT_COLOR 模式,或者干脆换 LightGroup 预设。
坑 8:换了天空盒,环境反射还是旧的。
AMBIENT_PREBAKED 用的是 LightGroup 里烘焙好的 cubemap,跟你的天空盒没有半点关系。要让反射跟上天空,得设 ambientSource = AMBIENT_SKYBOX 让它重新烘。金属材质在旧反射下会反射出一片不存在的天,这个在 PBR 场景里特别扎眼。
七、性能量级
项:代价
auto-exposure:多一组亮度直方图 RT,带宽接近翻倍,约一个全屏 pass
点光阴影(castShadows):6 面阴影图,多光源时是主要开销
环境光烘焙:后台线程,不阻塞渲染;同纹理结果缓存
Vignette / Tonemap:单个全屏 pass,量级很小
auto-exposure 适合明暗切换和物理光照单位项目;纯 2D、固定光照、移动端敏感项目别开。
八、边界:能做什么,做不了什么
能做:7 个 LightGroup 预设整体换氛围;点光/追光自由摆位;深度雾 + 高度雾同时开;程序化渐变天空、cubemap 全景、官方 9 个预制天空球;三种环境光来源运行时切换;暗角、自动曝光、全屏模糊、三种 tonemap。
做不了:SH 球谐系数和环境镜面 cubemap 没暴露给 Lua,要换只能整个换 LightGroup 预设;默认模式下 zone.ambientColor 是 no-op(引擎硬下发 cAmbientColor=(0,0,0,0),环境光由 SH 驱动),想用纯色得切到 AMBIENT_COLOR 模式;移动端没有 3D LUT 路径;GaussianBlur 只有开关,没有强度参数;ambientIntensity 默认 0.3,走烘焙模式时得手动推到 1.0,不然整体偏暗。
九、一条可迁移的判断方法
画面"配了没反应"时,按这三问走,比翻 shader 快得多:
1. 这个参数最终写进了哪个组件的哪次 pass? 写不进 shader 的属性,代码再对也没用。
2. 场景里有没有另一个同类型组件优先级更高? Zone 看 priority,材质看 technique 槽位。
3. 这条路径在当前图形后端上存在吗? LUT 只在桌面端,属性默认值可能不是你想的那个数。
三问都能答上来,再去读 shader 不迟。
(完)




