Unity Shader Graph实现《塞尔达传说》卡通渲染:从原理到实践
1. 项目概述:当《塞尔达传说:旷野之息》的卡通渲染遇上Unity Shader Graph
如果你玩过《塞尔达传说:旷野之息》,一定会对那种独特的视觉风格印象深刻——它既不是纯粹的卡通,也不是写实渲染,而是一种融合了手绘质感与光影层次的“卡通渲染”(Cel Shading)效果。这种风格让游戏世界看起来像一幅会动的、充满空气感的油画。很多开发者都想知道,这种效果在Unity里怎么实现?今天,我们就来彻底拆解一个名为“Breath of the Wild Cel Shading in Unity Shader Graph”的开源项目,看看如何利用Shader Graph这个可视化工具,一步步复现出林克冒险世界里的那种标志性着色风格。
这个项目本质上是一个教学性质的Shader Graph工程文件。它不只是一个简单的卡通描边,而是试图捕捉《旷野之息》中那种复杂的光影过渡、柔和的阴影边缘以及独特的色调映射。对于想深入学习风格化渲染、理解卡通着色核心原理,或者希望为自己项目增添独特美术风格的开发者来说,这是一个绝佳的切入点。即使你之前没怎么接触过Shader编程,通过Shader Graph的节点连线,也能直观地理解背后的图形学逻辑。接下来,我会带你从设计思路开始,一步步拆解节点,直到最终应用到模型上,并分享我在复现和调试过程中踩过的坑和总结的技巧。
2. 核心思路解析:如何解构“旷野之息”的视觉语言
在动手连接节点之前,我们必须先想清楚目标。《旷野之息》的卡通渲染之所以独特,是因为它做了好几层“减法”和“控制”,而不是简单地给模型描个黑边。
2.1 核心视觉特征拆解
首先,我们得把眼睛看到的效果,翻译成图形学可以处理的技术点。我总结下来,主要有这么几个关键特征:
- 阶跃式的光影过渡(Ramp Shading):这是卡通渲染的灵魂。它不像PBR(基于物理的渲染)那样有平滑连续的光照变化,而是将光照强度(比如兰伯特模型的N·L点积结果)映射到有限的几个色阶上。在《旷野之息》中,这种过渡通常非常柔和,不是生硬的三段式,而是带有一定渐变的多级过渡,这让物体在保持卡通感的同时又不失体积。
- 柔和的、带有颜色的阴影:阴影边缘不是锐利的,而是有一个轻微的渐变。更重要的是,阴影本身不是纯黑色或深灰色,而是常常带有环境光的颜色(比如天空的蓝色或草地的绿色),这使得阴影融入了环境,画面非常透气。
- 风格化的高光与反射:高光区域形状可控,有时是圆点,有时是条状,强度很高但范围很小。反射信息(如天空盒)会以非物理的方式混合到表面颜色中,增强画面的绘画感。
- 基于视角的轮廓线(Outline):也就是我们常说的“描边”。但《旷野之息》的描边很聪明,它并非对所有边缘一视同仁。比如,角色离相机很远时,描边可能变细或消失;某些特定材质(如金属)的描边颜色和强度也可能不同。这需要一种比简单的背面挤出(Backface Extrusion)更精细的控制方法。
- 全局的色彩调和与后处理:整个画面有一种统一的色调倾向,对比度和饱和度都经过精心调整,给人一种温暖、明亮又带点复古的感觉。
2.2 技术方案选型:为什么用Shader Graph?
实现上述效果,传统上需要编写复杂的表面着色器(Surface Shader)或顶点/片元着色器。这对于美术同学或不常写Shader的程序来说门槛很高。而Unity的Shader Graph完美解决了这个问题:
- 可视化与迭代快:所有算法都通过节点连接呈现,调整一个参数,效果立即可见。这对于调试光影过渡曲线、颜色混合权重等需要反复尝试的工作来说,效率提升巨大。
- 易于理解与教学:节点化的逻辑链条,让“兰伯特光照”、“菲涅尔效应”、“屏幕空间法线”这些概念变得可视。你可以清楚地看到数据(比如一个从0到1的值)是如何在各个节点间流动并最终影响像素颜色的。
- 与渲染管线兼容性好:无论是内置渲染管线(通过Lightweight RP或Universal RP的Shader Graph包),还是高清渲染管线(HDRP),Shader Graph都能很好地集成,并自动处理一些管线相关的输入(如主光源方向、阴影贴图)。
在这个项目中,我们将主要基于Universal Render Pipeline(URP)来构建,因为URP是目前Unity跨平台项目的首选,对移动端和PC端都有良好的支持,并且其Shader Graph功能非常成熟。
3. 环境准备与项目搭建
在开始复现效果之前,确保你的开发环境是正确配置的。这一步没做好,后面可能会遇到各种奇怪的报错或效果不对。
3.1 创建URP项目与导入Shader Graph
首先,你需要一个使用URP模板创建的Unity项目。如果你已有项目,需要先安装URP和Shader Graph包。
- 创建新项目:打开Unity Hub,点击“新建项目”。在模板列表中,选择“Universal Render Pipeline (URP)”。给你的项目起个名字,比如“BotWShaderGraphStudy”。
- 检查包管理器:项目创建好后,打开菜单栏的
Window > Package Manager。确保在“Unity Registry”中,你已经安装了以下两个核心包:- Universal RP:版本建议选择最新的稳定版(如14.x)。
- Shader Graph:版本需与URP兼容(通常URP包会依赖一个特定版本的Shader Graph)。
- 配置URP Asset:在Project窗口中,你应该能看到一个名为
UniversalRP-HighQuality或类似的URP配置文件(扩展名为.asset)。你可以双击它进行查看。对于我们的卡通渲染,有几个关键设置可以调整:- 主光源阴影(Main Light Shadows):确保开启。卡通渲染也需要真实的阴影投射来增强空间感。
- 抗锯齿(Anti-aliasing):建议开启MSAA或FXAA。卡通渲染的硬边缘在锯齿下会非常明显。
- 后期处理(Post-processing):建议开启。我们后续可能会加入一些全屏的色彩调整效果。
3.2 理解Shader Graph的基本工作流
在动手做“旷野之息”风格之前,我们先快速过一遍Shader Graph的标准操作,确保你对这个工具不陌生。
- 创建Shader Graph文件:在Project窗口中右键,选择
Create > Shader Graph > URP > Lit Shader Graph。这会创建一个基于URP光照模型的着色器图,它已经预置了PBR需要的输入输出块(如Base Color, Normal, Metallic等),我们将在其基础上修改。 - 认识工作区:打开创建的Shader Graph文件,你会看到几个主要区域:
- 主视图(Graph):连接节点的画布。
- 主预览(Main Preview):实时显示着色器在一个球体或指定模型上的效果。
- 黑板(Blackboard):这里定义着色器的属性(Properties),比如颜色、浮点数、纹理等。这些属性最终会暴露在材质的Inspector面板上。
- 节点创建菜单:在Graph中右键或按空格键可以搜索并创建各种功能节点。
- 第一个测试:改变基础色:
- 在Blackboard上点击“+”,添加一个
Color属性,命名为_BaseColor。 - 将这个属性从Blackboard拖到Graph中,它会自动创建一个属性节点。
- 找到名为
Fragment的Master Stack(主堆栈),展开后找到Base Color输入端口。 - 将
_BaseColor节点的输出线连接到Base Color输入端口。 - 此时,主预览中的球体应该变成了你属性节点上设置的颜色。保存这个Shader Graph(Ctrl+S)。
- 在Blackboard上点击“+”,添加一个
- 创建并应用材质:
- 在Project窗口中右键,选择
Create > Material,创建一个新材质球。 - 选中这个材质球,在Inspector面板的
Shader下拉菜单中,找到Shader Graphs分类,选择你刚刚创建并保存的Shader Graph文件。 - 现在,材质球上就出现了
_BaseColor属性,修改它,材质颜色随之改变。 - 将一个3D模型(比如一个Cube或Sphere)拖入场景,将这个材质球拖到模型上,效果就应用成功了。
- 在Project窗口中右键,选择
注意:很多新手会忘记保存Shader Graph文件。Unity不会自动保存Graph的修改。每次在Graph中连线或调整后,务必点击窗口左上角的Save Asset按钮,否则你的修改不会生效到材质上。
4. 核心效果实现:分步构建着色器节点网络
现在,我们进入核心部分,一步步搭建“旷野之息”风格的着色器。我会将复杂的效果拆解成几个独立的模块,最后再组合起来。
4.1 模块一:实现阶跃式光影(Ramp Shading)
这是卡通感的基石。我们不直接使用平滑的光照,而是将其“量化”。
获取光照信息:
- 我们需要一个关键数据:表面法线(Normal)与主光源方向(Light Direction)的点积(Dot Product)。这代表了光线照射到表面的强度(N·L)。
- 在Graph中右键,搜索
Dot Product节点并创建。 - 第一个输入(A):我们需要表面的法线。添加一个
Normal Vector节点(搜索Normal),将其输出连接到Dot Product的A端口。 - 第二个输入(B):我们需要主光源方向。添加一个
Main Light Direction节点(搜索Main Light),将其输出连接到Dot Product的B端口。 - 原理:
Normal Vector节点默认输出的是物体空间下的法线,而Main Light Direction输出的是世界空间下的方向。直接点积会出错。因此,我们需要将法线转换到世界空间。在Normal Vector节点后添加一个Transform节点,将From设置为Object Space,To设置为World Space,再将转换后的结果给Dot Product的A端。
重映射与阶跃:
- Dot Product的结果范围在[-1, 1]之间(背光面为负)。我们需要将其重映射到[0, 1]的范围,作为光照强度的基础值。添加一个
Remap节点,将In Min Max设为 (-1, 1),Out Min Max设为 (0, 1)。将Dot Product的结果输入。 - 现在,我们有一个0到1的平滑光照值。为了产生阶跃,我们需要一个“阶梯函数”。这里常用
Sample Gradient节点配合一个自定义的渐变纹理(Ramp Texture)来实现。 - 在Blackboard上添加一个
Texture 2D属性,命名为_RampTex。将其拖入Graph。 - 添加一个
Sample Texture 2D节点,将_RampTex连接到其Texture输入端口。 - 关键一步:将刚才重映射得到的 [0,1] 光照值,作为UV的U坐标去采样Ramp纹理。创建一个
Vector2节点,将其X分量设置为重映射后的光照值,Y分量可以固定为0(或者用一个参数控制)。将这个Vector2连接到Sample Texture 2D的UV端口。 - 制作Ramp纹理:你可以在Photoshop等软件中创建一个1xN像素的渐变条。例如,一个1x4的纹理,从左到右的像素颜色可以是:深阴影色、中间调色、亮部色、高光色。这样,当光照值从0到1变化时,就会在这四个颜色间跳跃。
- Dot Product的结果范围在[-1, 1]之间(背光面为负)。我们需要将其重映射到[0, 1]的范围,作为光照强度的基础值。添加一个
融合基础色:
- 从Ramp纹理采样得到的颜色,需要与物体的基础色相乘,才能得到最终受光照影响的颜色。添加一个
Multiply节点。 - 将
Sample Texture 2D的RGBA输出连接到Multiply的A端口。 - 将之前定义的
_BaseColor属性节点连接到Multiply的B端口。 - 将Multiply的结果输出到Master Stack的
Base Color端口。
- 从Ramp纹理采样得到的颜色,需要与物体的基础色相乘,才能得到最终受光照影响的颜色。添加一个
至此,你已经实现了一个最基础的卡通着色。通过调整_RampTex,你可以轻松控制光影的阶数和颜色。
4.2 模块二:添加柔和的彩色阴影
《旷野之息》的阴影不是死黑的。我们要模拟环境光对阴影的染色。
区分阴影区域:
- URP的Shader Graph提供了一个
Receive Shadows节点(搜索Shadow)。将其输出连接到某个计算中,可以判断当前像素是否在阴影中(输出为0或1)。 - 更精细的方法是使用
Main Light Shadows节点组,它可以给出主光源的阴影衰减因子(一个0到1的值,0表示完全在阴影中)。
- URP的Shader Graph提供了一个
混合阴影颜色:
- 在Blackboard上添加一个
Color属性,命名为_ShadowColor,可以默认设为一种偏冷的深蓝色。 - 我们需要根据阴影强度,将基础色向阴影色混合。使用
Lerp(线性插值)节点。 Lerp的A端口输入正常光照下的颜色(即上一节Multiply的结果)。B端口输入_ShadowColor。T端口输入阴影强度(0到1的值)。这里可以用Main Light Shadows节点的输出,或者用一个简单的阈值处理Receive Shadows的结果(例如,小于0.5算阴影)。- 将
Lerp的结果作为新的Base Color输出。
- 在Blackboard上添加一个
实操心得:直接使用
Receive Shadows的0/1二值结果会导致阴影边缘非常生硬。一个更好的技巧是:将Main Light Shadows的阴影衰减值通过一个Smoothstep节点处理一下。Smoothstep可以让你定义一个“过渡区间”,比如从0.3到0.7,这样阴影边缘就会有一个平滑的渐变,看起来更柔和、更自然。这正是“旷野之息”风格中阴影边缘柔和的秘密之一。
4.3 模块三:构建基于视角的轮廓线
背面挤出法简单但问题多(如内部轮廓、缩放问题)。这里介绍一种更健壮的“屏幕空间法线外扩”方法。
- 原理:在片元着色器中,我们根据视线方向与表面法线的夹角来决定边缘强度。夹角越大(即我们正对着表面),边缘越弱;夹角接近90度(即我们看向边缘),边缘越强。这可以通过法线与视角方向的点积来实现。
- 节点实现:
- 添加一个
View Direction节点,获取当前像素指向相机的方向(世界空间)。 - 添加一个
Normal Vector节点并通过Transform节点转换到世界空间,得到世界空间法线。 - 对这两个向量进行
Dot Product操作。当点积结果接近0时,说明视角与法线垂直,处于边缘;结果接近1时,说明视角与法线平行,处于正面。 - 我们需要反转这个关系:边缘处输出强,正面处输出弱。使用一个
One Minus节点处理点积结果。 - 添加一个
Power节点。将上一步的结果输入Power,并设置一个指数(如5.0)。Power节点可以让接近1的值衰减得更快,从而让轮廓线更细、更锐利。 - 现在你得到了一个边缘遮罩(Edge Mask),值在0到1之间,边缘处接近1。
- 添加一个
- 应用轮廓色:
- 在Blackboard添加一个
Color属性_OutlineColor(通常为黑色或深色)和一个Float属性_OutlineWidth。 - 使用
Lerp节点。A端口输入当前片元的颜色(已经过光照和阴影计算)。B端口输入_OutlineColor。T端口输入经过Power处理后的边缘遮罩。 - 将
Lerp的结果作为最终的Base Color输出。这样,在边缘处,颜色就会向轮廓色混合。 _OutlineWidth属性可以用来控制Power节点的指数,指数越大,轮廓线越细。
- 在Blackboard添加一个
这种方法生成的轮廓线质量很高,且不受模型背面或内部结构的影响,是当前实现风格化轮廓的主流方案。
4.4 模块四:风格化高光与后期调色
风格化高光:
- 使用
Specular相关节点计算高光强度(如Blinn-Phong模型)。 - 同样,将高光强度通过一个
Step或Smoothstep节点进行阈值化,产生“有”或“无”的硬边高光,或者小范围的渐变高光。 - 可以单独用一个
Color属性_SpecularColor来控制高光颜色,通常比基础色更亮、饱和度更高。 - 使用
Add节点将高光颜色叠加到Base Color上。
- 使用
简单的后期调色:
- 在Shader Graph的最后输出阶段,可以加入全局的颜色调整。
- 添加
Color属性_GlobalTint,使用Multiply节点与最终颜色相乘,实现整体色调偏移。 - 添加
Float属性_Saturation和_Contrast,利用Saturation和Contrast节点(可能需要自己用Lerp与灰度值计算来构建)进行调整。
将所有模块按逻辑顺序连接起来,你的Shader Graph网络可能会变得比较复杂。合理的分组和注释至关重要。你可以使用Sticky Note(便签)来为每个功能模块添加说明,并使用Group功能将相关的节点框选在一起,让整个图看起来清晰可维护。
5. 材质配置、场景调试与性能考量
着色器写好了,但要让它在场景里看起来对,还需要正确的材质和场景设置。
5.1 材质参数调节心法
创建一个使用你Shader Graph的材质,你会看到暴露出来的一堆属性。调节它们需要一些美术感觉和技巧:
_RampTex:这是控制光影氛围的核心。尝试不同的渐变纹理。一个典型的“旷野之息”风格Ramp可能只有3-4个色阶,中间调部分占比很大,亮部和暗部对比柔和。_ShadowColor:不要用纯黑。尝试用场景中主要环境光的补色或邻近色。比如在草原场景,阴影可以带一点青绿色;在黄昏场景,阴影可以偏紫。_OutlineWidth(通过Power指数控制):对于角色,轮廓线可以明显一些;对于环境物体,轮廓线应该非常细甚至关闭,以避免画面显得“脏”。- 光照与阴影设置:在Unity的Lighting窗口中,确保场景有合适的环境光(Environment Lighting)。Directional Light(平行光)的强度和颜色会极大影响卡通渲染的效果。可以尝试将光源颜色调得偏暖(如淡黄色),让画面更明媚。
5.2 常见问题与排查技巧实录
在复现过程中,你几乎一定会遇到下面这些问题。这里是我的排查清单:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 模型全黑或全白 | 1. Shader Graph未保存。 2. Master Stack的输出端口连接错误或未连接。 3. 法线信息错误。 | 1. 首先检查Shader Graph窗口左上角是否有“*”未保存标识,立即保存。 2. 检查 Base Color、Normal等主要输出端口是否都有正确的数据流入。3. 检查模型导入设置中是否勾选了“Read/Write Enabled”和“Normals”计算选项。在Shader Graph中临时将Base Color固定为一个纯色(如红色),看是否显示,以排除颜色计算错误。 |
| 轮廓线出现在模型内部或不均匀 | 1. 使用了背面挤出法,且模型非流形或法线有问题。 2. 基于视角的轮廓线算法中, View Direction或Normal空间错误。 | 1. 放弃背面挤出法,改用本文介绍的屏幕空间法线外扩法。 2. 确保 View Direction和Normal Vector在同一个空间下计算(通常都是世界空间)。添加一个Debug节点,将边缘遮罩的值直接输出为Base Color,观察其分布是否合理。 |
| 阴影边缘过于生硬 | 直接使用了二值的阴影判断。 | 使用Main Light Shadows节点的阴影衰减值,并通过Smoothstep节点进行平滑处理。调整Smoothstep的上下限,可以控制阴影渐变的范围。 |
| Ramp着色没有变化 | 1. Ramp纹理采样UV设置错误。 2. 光照计算错误。 | 1. 确保用于采样Ramp的UV的X分量确实是重映射后的N·L值。可以创建一个Debug节点,将这个值输出为灰度颜色,查看其是否在模型表面正确地从0(背光)渐变到1(向光)。2. 检查 Dot Product的输入法线和光源方向是否在同一个坐标系,并确认Main Light Direction节点在场景中有激活的平行光时能正确获取数据。 |
| 在Build后效果丢失或出错 | Shader Graph或URP的变体(Variants)没有正确打包。 | 1. 在Project Settings > Graphics > Shader Stripping 中,确保没有过度剥离变体。 2. 检查Shader Graph的Graph设置中,是否勾选了所有需要的特性(如阴影接收)。 3. 最稳妥的方法:将调试好的Shader Graph加入到某个Always Included Shaders列表中。 |
5.3 性能优化提示
卡通渲染Shader通常比复杂PBR Shader要省性能,但节点过多也会带来开销。
- 精简节点:检查是否有重复的计算。例如,世界空间法线如果被多个模块使用,应只计算一次,然后用一个
Branch节点或直接连线复用。 - 慎用复杂节点:
Sample Texture 2D是相对耗时的操作,确保你的Ramp纹理尺寸足够小(如1x256或更小)。Noise、Triplanar等节点在片元着色器中使用时需谨慎。 - 利用Shader LOD:在Shader Graph的Graph设置中,可以设置不同LOD(细节级别)下的简化版本。对于远处的物体,可以使用更简单的、关闭了轮廓线或高光的效果。
- 变体管理:通过
Keyword节点和Blackboard上的Dropdown属性来创建功能开关(如_ENABLE_OUTLINE),让美术可以根据需要开启/关闭某些效果,避免生成无用变体。
通过这个“Breath of the Wild Cel Shading in Unity Shader Graph”项目的学习和实践,你收获的不仅仅是一个酷炫的着色器。更重要的是,你掌握了如何使用节点化的思维去解构和实现复杂的视觉风格,理解了卡通渲染背后的核心图形学原理。Shader Graph将看似高深的技术拉下了神坛,让视觉开发变得直观而有趣。你可以以此为基础,尝试调整Ramp纹理做出不同的情绪氛围,修改轮廓线算法适应各种角色风格,甚至将这种思路应用到水体、云朵等特效上,创造出属于你自己的独特游戏世界。