从半透明模型的描边到URP Multipass Rendering
Table of Contents
请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com
卡通渲染技术中,描边的实现的有多种做法,有基于后处理实现,也有基于非后处理,通过多次绘制模型的方法实现。
通过多次绘制模型的实现,方法无非就是:
- 绘制模型本体,同时写入深度值。
- 让模型顶点沿着法线外扩一定的偏移量,即让模型膨胀一些
- 全用描边的颜色绘制膨胀过的模型,且,在绘制时,使用cull front,即只绘制模型的背向摄像机面。在第1步中因为绘制模型本体时已经写入深度值。在本步骤中只要不改变深度测试函数的方式,那么除了膨胀部分之外,是无法写入像素的,能写入的部分,便形成了边缘。
上述的方式,一般应用于不透明的材质上。如果是半透明的材质的话就有问题,因为在第1步绘制半透明材质时,一般是不写入深度值的。那么第3步在绘制膨胀模型时,模型背部的地方就会被绘制上去。
解决这个问题有很多,可以利用stencil test的方式。也可以在第一步绘制,不绘制任何颜色,只写入深度,然后再绘制模型,最后再绘制描边。后一种方法,如果用Unity内置管线,大约就是这个样子:
Shader "Unlit/半透明"
{
Properties
{
_Diffuse("Diffuse",Color) = (1,1,1,1) // 漫反射
_MainTex("MainTex",2D) = "white"{} // 2D纹理贴图
_AlphaScale("Alpha Scale",Range(0,1)) = 1 // 控制Alpha参数
}
SubShader
{
Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" } //渲染顺序设置Transparent
LOD 100
// 用两个pass通道来处理,防止出现渲染错误,第一个pass通道
// 每个pass通道都会渲染一次
Pass
{
ZWrite On // 注意这里:写入深度 为了确认渲染顺序
ColorMask 0 // 掩码遮罩代表这个pass通道不写入任何颜色值
}
Pass // 第二个pass通道
{
Tags{"LightMode" = "ForwardBase"}
ZWrite Off // 注意这里:关闭ZWrite(深度写入)
Blend SrcAlpha OneMinusSrcAlpha // 源颜色因子 正常透明混合
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f
{
float4 vertex :SV_POSITION; // 输出顶点信息
fixed3 worldNormal : TEXCOORD0;
float3 worldPos:TEXCOORD1;
float2 uv:TEXCOORD2;
};
fixed4 _Diffuse;
float _AlphaScale;
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert(appdata_base v) // 顶点着色器
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldNormal = worldNormal;
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed4 texColor = tex2D(_MainTex, i.uv);
fixed3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);
fixed3 diffuse =
texColor.rgb * _LightColor0.rgb * _Diffuse.rgb *
max(0, dot(worldLightDir,i.worldNormal) * 0.5 + 0.5);
fixed3 color = ambient + diffuse;
return fixed4(color, texColor.a * _AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
内置管线的方式很直观。但URP到目前为止(版本10.3.2),无法支持太多的PASS,
打开【com.unity.rende-pipelines.unversal@10.3.2/Runtime/Passes/DrawObjects.cs】文件可以看到:
public class DrawObjectsPass : ScriptableRenderPass
{
FilteringSettings m_FilteringSettings;
RenderStateBlock m_RenderStateBlock;
List<ShaderTagId> m_ShaderTagIdList = new List<ShaderTagId>();
string m_ProfilerTag;
ProfilingSampler m_ProfilingSampler;
bool m_IsOpaque;
static readonly int s_DrawObjectPassDataPropID = Shader.PropertyToID("_DrawObjectPassData");
// 注意观察参数ShaderTagId[] shaderTagIds
public DrawObjectsPass(string profilerTag, ShaderTagId[] shaderTagIds,
bool opaque, RenderPassEvent evt, RenderQueueRange renderQueueRange,
LayerMask layerMask, StencilState stencilState, int stencilReference)
{
base.profilingSampler = new ProfilingSampler(nameof(DrawObjectsPass));
m_ProfilerTag = profilerTag;
m_ProfilingSampler = new ProfilingSampler(profilerTag);
foreach (ShaderTagId sid in shaderTagIds)
m_ShaderTagIdList.Add(sid);
renderPassEvent = evt;
m_FilteringSettings = new FilteringSettings(
renderQueueRange, layerMask);
m_RenderStateBlock = new RenderStateBlock(RenderStateMask.Nothing);
m_IsOpaque = opaque;
if (stencilState.enabled)
{
m_RenderStateBlock.stencilReference = stencilReference;
m_RenderStateBlock.mask = RenderStateMask.Stencil;
m_RenderStateBlock.stencilState = stencilState;
}
}
// 注意观察参数new ShaderTagId
public DrawObjectsPass(string profilerTag, bool opaque,
RenderPassEvent evt, RenderQueueRange renderQueueRange,
LayerMask layerMask, StencilState stencilState, int stencilReference)
: this(profilerTag,
new ShaderTagId[] {
new ShaderTagId("SRPDefaultUnlit"),
new ShaderTagId("UniversalForward"),
new ShaderTagId("UniversalForwardOnly"),
new ShaderTagId("LightweightForward")},
opaque, evt, renderQueueRange, layerMask, stencilState, stencilReference)
{}
}
所以为了在URP中解决这个问题,一般采用双材质球的方式,去把这些多个pass拆分到两个或者多个材质球去完成。在第一个材质球中,第一个pass的LightMode为SRPDefaultUnlit。在此pass中只写入深度,第二个pass的LightMode为"UniversalForward"。绘制描边,然后在第二个材质球中,在第一个pass中负责绘制半透明模型即可。