> 文章列表 > 【Unity URP】PBR转NPR风格化场景01:描边

【Unity URP】PBR转NPR风格化场景01:描边

在 Unity 的 URP 中实现风格化描边效果,可以通过双Pass方法结合Stencil缓冲来实现。首先,PBR(基于物理的渲染)和NPR(非光栅化渲染)的区别在于前者更真实,而后者更具艺术性。NPR风格化的关键是根据需要调整光照和材质,添加如描边等视觉效果。

通过双Pass方法,URP中的描边实现分为两个Pass:第一Pass正常渲染模型,第二Pass则向外扩展模型的顶点,形成描边轮廓。通过Stencil缓冲,可以精确控制哪些像素被保留或剔除,确保只有扩展的轮廓部分被渲染,而其他区域被隐藏。

在URP中,多Pass渲染需要通过设置不同的LightMode标签实现,而内置的Lit着色器提供了多个Pass的示例,每个Pass都有不同的标签,确保相同的标签只会被执行一次,从而支持多Pass渲染。

Stamp技术虽然适合小范围效果,但在风格化渲染中可能不如双Pass方法高效。因此,结合Stencil缓冲的双Pass方法在实现描边效果时更为实用。

整体而言,通过合理配置Shader标签和渲染管线,可以有效地在URP中实现风格化描边,提升画面的艺术性。同时,需要注意性能优化,避免不必要的性能瓶颈。

【Unity URP】PBR转NPR风格化场景01:描边

写在前面

风格化不像PBR,好像没有套路可言,,,简直是《怎么好看怎么来》的最大化实践了!感觉出的PBR+NPR也是为了更好地利用PBR资产才诞生的这样一个渲染方案。(当然我的评价非常非常的片面,瞎说的)

偶然间看到了b站一位大佬在blender里实现的效果(原链接【blender】传统PBR转风格化三渲二无主之地风格,作者甚至还提供了Blender源文件,感恩TAT):

直接截图的Blender源文件打开的场景,侵删

嗷嗷嗷是我非常喜欢的风格!无主之地从场景到人物都点在我的审美上,,,我要Copy到Unity里!!

先在blender里尝试一下这个渲染方案对贴图的要求高不高吧,验证一下可行性,拿了一个之前从Bridge下载(题外话,,Bridge真得很好用啊啊啊素材很多很方便)的基础木箱模型:

标准PBR效果
改后效果(PBR+NPR)

我给强行融入到上面的场景中了hhhh,感觉还不错!说明这套方案对传统PBR模型+贴图资产直接着色的效果可以的!

由于URP各个版本更新换代太快了,贴一下项目环境,给后面看到这篇文章的小伙伴提个醒,我的项目环境:

URP12.1.7

Unity2021.3.8f1

那那那,开始复刻!


首先就是描边了,也是本文的重点。主要涉及了两种双Pass实现描边的方案(边缘检测没涉及,之前写过了,感兴趣可以看看(1条消息) 【Unity Shader】屏幕后处理2.0:实现Sobel边缘检测)

观察了一下Blender方案里描边也是NPR传统的外扩描边思路(只不过Blender里要给模型实例化修改器再剔除描边),一个材质完全负责描边,一个则负责着色:

换到Untiy下的话应该要么就是双Pass,要么就是URP下RenderFeature实现,二者一个意思。(之前水墨那个效果的描边是直接基于观察方向+法线方向实现的,出来的描边效果不“硬”,却符合水墨的那种随意感,但感觉并不适合正常NRP的描边方式。)

我们先讨论双Pass实现描边效果,

1 模板测试实现描边

1.1 补一下Stencil的知识

在Stencil实现描边效果的时候,会有:

Stencil
{Ref [_StencilID]Comp AlwaysPass ReplaceFail Keep
}

在之前学习渲染管线时:【技术美术图形部分】图形渲染管线3.0-光栅化和像素处理阶段中就接触到了Stencil,也就是模板测试,在最后的逐片元操作中每个片元需要通过层层关卡(测试),最终才能被展示出来:

简单概括,Stencil可以用于在渲染中筛选和保留像素,而且是高度可配置的,配置的话就像最开始举例的一样,需要给一些参数定义值。参考Unity - Manual: ShaderLab command: Stencil,可配置项有,

Stencil
{Ref <ref>ReadMask <readMask>WriteMask <writeMask>Comp <comparisonOperation>Pass <passOperation>Fail <failOperation>ZFail <zFailOperation>CompBack <comparisonOperationBack>PassBack <passOperationBack>FailBack <failOperationBack>ZFailBack <zFailOperationBack>CompFront <comparisonOperationFront>PassFront <passOperationFront>FailFront <failOperationFront>ZFailFront <zFailOperationFront>
}

 比较基础的配置项可以是这样,

Stencil
{Ref 2Comp equalPass keepZFail decrWrap
}

Ref

Ref中选定的是Stencil ID,0-255,默认0。用于和模板缓冲(Stencil Buffer)中的值比较,如何比较就是后面的Comp中给定,满足条件就保留,不满足就剔除掉。

Comp

比较方式,直接定义就行。具体的话有:

Pass

Stencil operation值,当片元通过上面的Comp比较后,这里可以定义通过测试后的操作,决定他是留下?还是写入0?还是其他的操作,默认的是Keep(保留)。可取值如下:

Fail

也是Stencil Operation值,道理和Pass一样,如果没有通过测试,该对片元执行的操作,操作赋值方式跟Pass的一样。

zFail

当前片元通过模板测试但是没通过深度测试,该执行什么操作?赋值同样跟Pass的一样。

1.2 双Pass描边原理

两个Pass分工明确,

  • Pass1:正常渲染正面面片
  • Pass2:渲染背面面片,并用某些技术仅让它多出的轮廓可见

我们先尝试用Stencil进行,那么具体过程就是,

  • PASS1:给Stencil Buffer刷特定值,并在当前Pass进行正常的渲染操作
  • PASS2:进行描边,先把模型向外延伸,把模型顶点沿着法线方向向外扩张一段距离,这段距离就是描边的厚度了,再通过调整参数仅渲染扩张的部分,输出描边色就行

关于该方法的优点和缺点,我们后面再进行讨论。

1.3 关于URP中的双Pass

从其他文章看到的说法:“URP下双Pass是有代价的,shader无法被SRP batching机制优化。”就是说最好别多Pass的意思?但是Lit里也有多Pass诶,,这里先持怀疑态度~~因为很可能URP后来推出了能够参与SRP batching的多Pass方案也说不定呢。

URP渲染Pass的方式

Build-in下的多Pass如果直接搬到URP下会不奏效,很多文章直接说URP下只支持单Pass,但其实是换了一种方式,从按Pass分的方式变成了按LightMode分。我们打开内置的Lit.shader看看源码:

Lit里也是很多Pass!但每个Pass都有不同的Tag,正常光照的打了

Tags{"LightMode" = "UniversalForward"}

阴影的打了

Tags{"LightMode" = "ShadowCaster"}

等等等等,Unity URP中的Single-Pass到底是什么中举了例子很好地说明了这一点。我直接copy过来他最终的结论:相同的Tags标签只会被执行一次,而不是说一个shader里面只能有一个Pass块。

查看RenderObjectsPass

这里我们可以把项目Packages下的Universal RP文件在VS Code打开,就可以查找想要的.cs文件啦!我们找到RenderObjectsPass.cs文件,

        public RenderObjectsPass(string profilerTag, RenderPassEvent renderPassEvent, string[] shaderTags, RenderQueueType renderQueueType, int layerMask, RenderObjects.CustomCameraSettings cameraSettings){base.profilingSampler = new ProfilingSampler(nameof(RenderObjectsPass));m_ProfilerTag = profilerTag;m_ProfilingSampler = new ProfilingSampler(profilerTag);this.renderPassEvent = renderPassEvent;this.renderQueueType = renderQueueType;this.overrideMaterial = null;this.overrideMaterialPassIndex = 0;RenderQueueRange renderQueueRange = (renderQueueType == RenderQueueType.Transparent)? RenderQueueRange.transparent: RenderQueueRange.opaque;m_FilteringSettings = new FilteringSettings(renderQueueRange, layerMask);if (shaderTags != null && shaderTags.Length > 0){foreach (var passName in shaderTags)m_ShaderTagIdList.Add(new ShaderTagId(passName));}else{m_ShaderTagIdList.Add(new ShaderTagId("SRPDefaultUnlit"));m_ShaderTagIdList.Add(new ShaderTagId("UniversalForward"));m_ShaderTagIdList.Add(new ShaderTagId("UniversalForwardOnly"))