1. 项目概述与核心思路
最近在做一个风格化场景的项目,美术同学想实现一种“伪室内”的效果,简单来说,就是让一个在户外的建筑模型,看起来像是内部有灯光透出来,营造出窗户后面有房间、有生活的感觉。这种效果在赛博朋克、低多边形或者一些需要快速搭建氛围的场景里特别有用,能极大地提升场景的叙事感和细节丰富度。如果用传统建模方式去真的搭建室内,工作量巨大,而且对性能也是不小的负担。所以,我们决定用ShaderGraph在Unity里通过着色器“骗”过玩家的眼睛。
这个“伪室内”效果的核心思路,其实就是利用一张或几张表现室内场景的贴图(我们称之为“室内贴图”或“窗户贴图”),通过着色器技术,将其“贴”到建筑模型的窗户、门洞等镂空部分,并配合光照、模糊、动态等处理,模拟出从内部透光的效果。它本质上是一种基于屏幕空间或模型UV空间的“贴花”与“光照模拟”技术的结合。我花了几天时间研究和调试,最终用ShaderGraph搭建了一套比较稳定且效果可控的方案,不仅实现了静态的室内透光,还能模拟简单的动态(比如灯光闪烁、电视屏幕变化),性能开销极低。下面我就把这套方案的完整设计思路、节点连接、参数调试以及踩过的坑,毫无保留地分享出来。
2. 效果核心原理与方案选型
在动手搭建ShaderGraph之前,我们必须先想清楚要“骗”过眼睛,需要解决哪几个关键问题。这决定了我们整个着色器的架构。
2.1 效果拆解:我们需要模拟什么?
一个看起来真实的“室内透光”效果,通常包含以下几个视觉要素:
- 发光源:窗户区域本身应该作为光源,向外发射光线。这意味着窗户部分的亮度要远高于周围墙壁,并且能照亮附近的环境(即便很微弱)。
- 室内内容:透过窗户,应该能看到室内的景象,比如墙壁、家具的剪影、发光的灯具等。这部分内容需要有合理的透视和细节。
- 光线衰减与氛围:光线从室内穿过玻璃到室外,会有衰减。靠近窗户边缘的地方可能更亮,中心区域则能看清更多室内细节。同时,玻璃本身可能带有颜色、污渍或反射。
- 动态元素:真实的室内灯光可能闪烁(如老式日光灯)、电视屏幕会变化、有人影掠过。加入简单的动态能极大提升真实感。
2.2 技术方案对比与选型
基于以上要素,有几种常见的技术路径:
方案A:使用Emissive自发光材质这是最直接的方法,给窗户部分一个高强度的自发光材质。优点是简单、性能好。但缺点非常明显:它只是一个纯色或简单纹理的发光板,完全没有室内空间的纵深感和细节,看起来非常假,像一块广告牌。
方案B:在模型后方放置一个带室内贴图的Quad在建筑模型每个窗户的后面,真正放置一个带有室内贴图的平面。通过调整平面的位置和缩放,可以模拟出不错的透视效果。这种方法效果最好,最灵活。但缺点同样突出:每个窗户都需要一个额外的Draw Call和面片,管理复杂,性能随窗户数量线性增长,不适合大规模使用。
方案C:使用Shader进行UV空间映射(本项目方案)这是我们最终选择的方案。其核心思想是:将室内贴图“投射”到模型特定的UV区域(即窗户部分)。
- 遮罩:我们需要一张遮罩贴图(Mask Texture),来精确界定模型的哪些部分是窗户(白色),哪些部分是墙壁(黑色)。这张图通常可以在建模软件中烘焙,或者用模型的第二套UV配合程序化噪声生成。
- 映射:为室内贴图设计一套独立的UV坐标。这套坐标需要解决一个问题:如何让一张平面的贴图,在窗户这个“洞”里看起来有透视感?这里我们采用类似“视差映射”的简化思想,或者更简单地,使用一个与窗户形状匹配的、经过透视变换的UV。
- 合成:将采样到的室内贴图颜色,与模型本身的基础颜色(墙壁颜色)进行混合。混合的系数就是遮罩贴图的强度。同时,室内贴图的亮度贡献到自发光通道,实现发光效果。
为什么最终选择方案C?因为它完美平衡了效果、性能和灵活性。一张精心绘制的室内贴图可以包含丰富的细节;通过Shader计算,一个材质球就能处理整个建筑模型上所有的窗户,Draw Call不变;通过参数调节,可以轻松控制亮度、颜色、动态效果;而且完全可以与Unity的全局光照系统(如Baked GI)结合,让这些“伪光源”真正参与到场景光照中。接下来,我们就进入ShaderGraph,一步步实现它。
3. ShaderGraph核心节点网络搭建
打开Unity,创建一个新的ShaderGraph(URP或Built-in管线均可,节点名称可能略有差异,但逻辑相通)。我以URP为例进行说明。我们的Graph最终会有几个主要的属性块:室内贴图、遮罩贴图、UV变换参数、发光参数等。
3.1 基础颜色与遮罩混合
首先,我们建立最基础的通道:将室内贴图通过遮罩,“贴”到模型上。
创建属性:
BaseColor(Color):建筑墙壁的基础颜色。InteriorTexture(Texture2D):那张表现室内场景的贴图。建议使用一张横向的、带有透视感的房间角落贴图,这样适用于大多数窗户。MaskTexture(Texture2D):黑白遮罩贴图。白色区域代表窗户。BaseMap(Texture2D):可选,墙壁的纹理贴图。
UV处理:
- 将
UV节点连接到Sample Texture 2D节点采样BaseMap和MaskTexture。这里通常使用模型的主UV集(UV0)。 - 对于
InteriorTexture,我们不能直接使用模型UV,否则贴图会拉伸变形。我们需要一套独立的UV变换。- 添加
Tiling And Offset节点。创建属性Interior_Tiling(Vector2) 和Interior_Offset(Vector2)。 - 将模型的
UV节点连接到Tiling And Offset节点的UV输入,将Interior_Tiling和Interior_Offset属性连接到对应输入。这样,美术同学可以通过调整Tiling(缩放)和Offset(偏移),来让室内贴图的内容适配窗户的视野范围。例如,调整Offset的X值,可以模拟“看”向房间的不同角度。
- 添加
- 将
采样与混合:
- 用处理后的UV去采样
InteriorTexture。 - 使用
Lerp(线性插值)节点进行混合。将BaseColor(或采样BaseMap的结果)连接到A端口,将采样到的InteriorTexture颜色连接到B端口。 - 将采样
MaskTexture的红色通道(通常用来存储遮罩)连接到Lerp节点的T(插值系数)端口。这样,遮罩为0(黑色)的地方显示墙壁颜色,遮罩为1(白色)的地方显示室内贴图颜色。 - 将这个
Lerp的结果输出给主节点的Base Color。
- 用处理后的UV去采样
至此,一个静态的、基础的伪室内贴图效果就完成了。你可以通过调节遮罩贴图和室内贴图的UV,让室内景象出现在正确的窗户位置。
3.2 自发光与光照增强
静态贴图还不够,我们需要让它“发光”。
- 创建发光强度属性:
Emission_Intensity(Float), 可以设置一个较大的默认值,比如5或10。 - 计算发光颜色:将采样到的
InteriorTexture颜色与Emission_Intensity相乘,得到一个高亮度的颜色。这里可以再做一层处理,比如将颜色先转换为灰度,再乘以一个Emission_Color属性,这样可以单独控制发光的色相和强度,更加灵活。 - 遮罩控制发光区域:将上一步得到的高亮度颜色,再次与
MaskTexture的强度(可以是RGB通道的某种计算,如(R+G+B)/3,或者直接用R通道)相乘。确保只有窗户区域在发光。 - 输出:将最终计算得到的发光颜色,连接至主节点的
Emission输入口。
重要提示:在URP中,要使自发光影响周围环境(屏幕空间光照、Bloom后处理),需要确保材质的
Surface Type设置为Transparent(如果窗户部分是透明的)或Opaque但勾选了Add Precomputed Lighting,并且需要在后处理Volume中启用Bloom效果。发光的强度需要通过Emission_Intensity和Bloom的阈值配合调试。
3.3 模拟透视与深度感(进阶)
为了让平面的贴图看起来更有立体感,我们可以模拟一些简单的透视效果。
视差偏移(简易版):
- 原理:根据视角方向,轻微偏移室内贴图的UV,模拟视线穿过窗户看到室内不同部分的效果。虽然不如真正的视差贴图精确,但开销极小,能增加生动性。
- 实现:
- 获取视角方向:使用
View Direction节点,并转换为切线空间(Transform节点,World to Tangent)。 - 我们只关心XY方向(即平行于窗户平面的偏移)。将切线空间的View Direction的X和Y分量提取出来。
- 用这两个分量,分别乘以一个很小的系数属性
Parallax_Strength(例如0.05),再乘以遮罩强度(确保只有窗户区域有偏移),得到UV偏移量。 - 将这个偏移量加到
InteriorTexture的UV上,再去做采样。
- 获取视角方向:使用
- 效果:当摄像机移动时,室内的画面会随着视角有轻微的滑动,仿佛你真的在从不同角度窥视室内,立体感瞬间提升。
边缘光与衰减:
- 现象:真实窗户的透光,在窗户框边缘附近可能更亮(光线衍射),或者因为玻璃污渍导致中心与边缘亮度不一。
- 实现:
- 我们可以利用遮罩贴图本身。对
MaskTexture采样结果进行一些处理。例如,使用Smoothstep节点:Smoothstep(0.2, 0.8, Mask.r)。这样可以将遮罩的0-1过渡区域进行平滑,并压缩中间亮部区域的范围,使得窗户边缘的过渡带产生一个亮度变化,可以用来调制发光强度。 - 或者,单独采样一张用于控制亮度衰减的贴图(Ramp图),将其与遮罩结合,创造出中心亮、边缘暗,或边缘亮、中心暗等不同效果。
- 我们可以利用遮罩贴图本身。对
3.4 添加动态效果
静态的室内看起来还是像一张画。我们加入一些简单的动态。
灯光闪烁:
- 使用一个简单的三角函数(
Sine Time节点)来模拟周期性闪烁。Sine Time本身输出的是基于时间变化的正弦波。 - 创建一个属性
Flicker_Speed用于控制速度,Flicker_Intensity用于控制闪烁强度范围。 - 计算式:
Flicker = 1 + (sin(Time * Flicker_Speed) * Flicker_Intensity)。这样Flicker会在1±Intensity之间波动。 - 将计算出的
Flicker值,乘到之前计算好的发光颜色上。注意,为了更真实,可以只让室内贴图中代表“光源”(如灯泡、台灯)的特定颜色通道(比如高亮白色部分)参与闪烁,这需要更精细的贴图规划。
- 使用一个简单的三角函数(
电视屏幕噪波:
- 假设我们的室内贴图中有一块区域是电视屏幕。
- 我们可以使用一个动态的噪声图。ShaderGraph提供了
Gradient Noise节点。 - 将
Time节点与一个速度属性相乘,加到噪声UV的某个分量上,让噪声动起来。 - 然后,将动态噪声与电视屏幕区域的颜色混合(例如
Screen混合模式)。电视屏幕区域同样需要一张遮罩来界定(可以利用室内贴图的Alpha通道,或者第二张遮罩图)。
动态UV偏移(模拟人影):
- 这是更高级的效果。原理是让室内贴图的一部分(比如代表人影的深色区域)随时间缓慢移动。
- 我们可以准备两张室内贴图:一张基础图,一张带有人影叠加层的图。或者在一张贴图上,人影位于特定颜色通道。
- 使用一个缓慢变化的
Time信号去驱动Interior_Offset的微小变化,但只应用在代表人影的颜色信息上。这通常需要更复杂的节点网络,涉及颜色通道分离和条件混合。
4. 参数调节与美术资源制作要点
Shader搭建好了,效果的好坏七分靠Shader,三分靠美术资源。这里分享一些制作贴图和调节参数的心得。
4.1 遮罩贴图制作
遮罩贴图是效果的基石,必须精准。
- 精度:尽量使用高精度的贴图(如1024x1024),确保窗户边缘清晰锐利,没有锯齿。模糊的遮罩边缘会导致发光区域扩散到墙上,显得很脏。
- 绘制方式:在建模软件(如Blender, 3ds Max)中,将窗户部分的面单独分一个材质ID或顶点色,然后烘焙出一张纯黑白的遮罩贴图。这是最准确的方法。
- 备用方案:如果模型没有分好,可以在Unity里利用模型的第二套UV(UV1),配合程序化噪声来生成。但可控性较差。
4.2 室内贴图设计
这张图决定了“室内有什么”。
- 透视:强烈建议使用带有两点透视的室内场景照片或手绘素材。视角略微俯视或平视,这样贴在窗户上时,透视关系才显得自然。纯正面的视角会显得很平。
- 明暗对比:贴图本身应有明确的明暗关系。光源处(灯具、窗户对面)最亮,角落、家具背面要暗。这样在乘以发光强度后,高亮部分会更突出,暗部也不会过曝,保留细节。
- 色彩:色调要统一。暖光(钨丝灯)用橙黄色调,冷光(日光灯、电视)用蓝白色调。可以在Shader里用
Emission_Color属性整体染色,但贴图本身的色调是基础。
4.3 Shader参数调试清单
把关键参数列出来,方便调试时参考:
| 参数名 | 功能 | 典型值/范围 | 调试技巧 |
|---|---|---|---|
Interior_Tiling | 缩放室内贴图 | (1,1) 开始调节 | 值>1会放大室内细节(看得更近),值<1会缩小(看得更远)。用于匹配窗户大小和预期的观看距离。 |
Interior_Offset | 偏移室内贴图 | (0,0) 开始调节 | 微调X值可以“环顾”室内左右,微调Y值可以调整视线高低。这是调整构图的关键。 |
Emission_Intensity | 发光强度 | 5 - 20 | 白天场景调低,夜晚场景调高。需要与场景的后处理Bloom阈值配合调试。强度太高会导致Bloom过曝成一片白光。 |
Emission_Color | 发光颜色 | 白色 (1,1,1,1) | 用于统一改变室内光线的色温。例如,设置为淡黄色(1,0.9,0.7,1)模拟暖光灯。 |
Parallax_Strength | 视差强度 | 0.0 - 0.1 | 从0开始慢慢增加,直到摄像机移动时有轻微但可察觉的滑动感即可。太大反而会穿帮。 |
Flicker_Speed | 闪烁速度 | 1 - 10 | 模拟日光灯故障可以快一些(8-10),模拟烛光可以慢一些(1-3)。 |
Flicker_Intensity | 闪烁强度 | 0.0 - 0.3 | 控制亮度波动的幅度。0.1-0.2的微闪比较自然,0.3以上会显得灯光接触不良。 |
调试时,建议在目标运行平台(如PC、主机)的最终画质下进行,因为移动平台和PC平台的Bloom、色调映射等后处理效果差异很大。
5. 性能优化与常见问题排查
这个效果虽然不复杂,但在大规模使用(如整个城市建筑)时,仍需注意性能。
5.1 性能优化点
- 纹理合并:如果有多栋建筑使用不同的室内贴图,尽量将多张小图合并成一张大图集(Texture Atlas)。这样可以将多次采样合并为一次,减少纹理采样指令。遮罩贴图也可以合并。
- Shader变体管理:如果你的ShaderGraph包含很多可选的动态效果(如闪烁、噪声),通过Shader Keywords来控制。不要在一个Shader里包含所有功能,然后靠参数设为0来关闭,这可能导致不必要的变体和性能开销。应该为“静态版”和“动态版”创建不同的材质实例,或使用
Shader Variant分支。 - LOD支持:为远处建筑使用简化版本的Shader。例如,在远处可以完全关闭视差效果、动态效果,甚至降低室内贴图的分辨率(通过Mipmap)。
- 剔除优化:确保建筑模型使用了正确的遮挡剔除(Occlusion Culling)。因为我们的窗户是自发光,如果被其他建筑挡住,就不应该被渲染。
5.2 常见问题与解决方案
这里记录了几个我在测试中遇到的具体问题及解决方法。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 窗户发光边缘有锯齿或闪烁 | 1. 遮罩贴图分辨率不足。 2. 抗锯齿(MSAA/TAA)与Alpha Test或Alpha Blend的边界处理问题。 | 1. 提高遮罩贴图精度,或开启其导入设置的Generate Mip Maps并使用三线性过滤。2. 尝试将材质的 Alpha Clip Threshold稍微调高(如从0.5调到0.6),或改用Alpha Blend模式并调试混合模式。 |
| Bloom效果下窗户光晕过大,糊成一片 | Emission_Intensity值过高,超过了后处理Volume中Bloom效果的Threshold(阈值)和Intensity(强度)的合理范围。 | 1.优先降低材质Emission_Intensity,使其在不开Bloom时看起来亮度合适。2. 然后调整Bloom的 Threshold,确保只有最亮的部分(如灯具中心)能触发Bloom。3. 最后用Bloom的 Intensity控制光晕大小。三者需协同调试。 |
| 从某些角度观看,室内贴图“穿帮”或消失 | 1. 视差强度(Parallax_Strength)设置过大。2. 室内贴图UV偏移( Interior_Offset)导致有效画面移出视野。3. 模型法线方向错误,导致背面剔除。 | 1. 减小Parallax_Strength。2. 重置或调整 Interior_Offset。3. 检查模型法线,或在Shader中将 Cull Mode设置为Off(双面渲染),但会略微增加开销。 |
| 动态闪烁效果不自然,像机械开关 | 使用了简单的Sine Time,波形过于规则完美。 | 引入噪声进行调制。例如:Flicker = 1 + (sin(Time * Speed + Noise) * Intensity),其中Noise是一个缓慢变化的低频率噪声值。或者,使用Random Range节点配合时间取整,模拟不规则的闪烁间隔。 |
| 在移动平台上帧率下降明显 | 1. 使用了高分辨率纹理且未压缩。 2. 过于复杂的动态计算(如每帧采样多次噪声)。 3. 没有使用合批(Batch)。 | 1. 为移动平台使用ASTC压缩格式的纹理,并适当降低分辨率。 2. 简化或关闭动态效果。考虑使用顶点动画或纹理动画替代每像素的复杂计算。 3. 确保建筑模型材质相同,且符合Unity的静态/动态合批条件。 |
最后,再分享一个调试小技巧:在Scene视图中,将渲染模式切换到Overdraw或Shaded Wireframe模式,可以直观地看到Shader的渲染复杂度和覆盖范围,对于优化性能非常有帮助。伪室内效果是一个典型的“视觉把戏”,它的成功在于对细节的把握。多观察现实世界中窗户透光的现象,比如傍晚时分楼宇的灯光,不断调整你的遮罩、贴图和Shader参数,直到它看起来“感觉对了”为止。这个方案已经在我们好几个项目中成功应用,从赛博朋克的霓虹公寓到中世纪小镇的烛光橱窗,只需要更换贴图和调整参数,就能快速产出令人信服的效果,性价比极高。