在URP中,SurfaceShader已經(jīng)不再被支持了,學(xué)URP和HLSL去吧沉颂,別碰SurfaceShader了。
前言
??沒(méi)錯(cuò)悦污,又是老生常談的NRP(非真實(shí))渲染铸屉,或者說(shuō)卡通渲染。最近事情不多切端,研究了一下Shader方面感興趣的東西彻坛,首先試了一下用RenderTexture實(shí)現(xiàn)的實(shí)時(shí)MatCap,有點(diǎn)意思但找不到什么應(yīng)用場(chǎng)景,暫時(shí)丟到一邊了昌屉。然后不知道為什么又回到了NPR上面钙蒙,于是就照著現(xiàn)在公司項(xiàng)目中的渲染效果為參考來(lái)編寫了。最后的結(jié)果感覺(jué)完成度還可以间驮,所以拿出來(lái)吹逼一下躬厌。
Outline 描邊
??搞NPR的要點(diǎn)之一無(wú)疑就是描邊,關(guān)于這個(gè)竞帽,有件事就想提一下扛施。我寫Shader都是用SurfaceShader編寫。之前做描邊碰到的最大的問(wèn)題就是怎么實(shí)現(xiàn)描邊屹篓,因?yàn)槟菚r(shí)候看到不止一篇文章說(shuō)SurfaceShader沒(méi)有Pass疙渣,就非常僵硬。但是堆巧,這次我在搜索的時(shí)候居然發(fā)現(xiàn)原來(lái)SurfaceShader是可以加Pass的妄荔!所以描邊問(wèn)題自然就引刃而解了,感謝這篇文章谍肤。
??做法是傳統(tǒng)的Inverted-Hull啦租,代碼是從Toony Colors Pro 2插件中抄來(lái)的,翻譯了一下變成SurfaceShader可以用的代碼谣沸。支持多種途徑(通常刷钢、頂點(diǎn)顏色、切線乳附、UV)來(lái)控制描邊内地,支持固定寬度,支持通過(guò)一些參數(shù)微調(diào)赋除,效果蠻玄學(xué)的阱缓,不過(guò)總比不能調(diào)要好。
不要用Cutout
有時(shí)制作頭發(fā)或者衣服上有鏤空之類举农,會(huì)采用Cutout的做法荆针,在Shader里使用clip函數(shù)對(duì)像素進(jìn)行剔除。但是想要使用Inverted-Hull來(lái)做描邊的話颁糟,這種做法就不行了航背,因?yàn)槊柽吺歉鴐esh走的,剔除掉表面的像素并不會(huì)改變mesh的形狀棱貌,描邊就會(huì)出現(xiàn)問(wèn)題玖媚。而且面片對(duì)這種描邊方式本身就不友好,所以不要再用面片做頭發(fā)了婚脱,請(qǐng)做出體積今魔!
自定義光照:二刺螈
??二刺螈不需要漸變勺像!一般的Lambert模型的光照計(jì)算公式為dot(normal, lightDir) * atten
,最簡(jiǎn)單的做法——對(duì)其round一下就可以使明暗分離為兩層了错森。
??某些情況下吟宦,僅僅兩層的明暗關(guān)系可能不夠用,所以使Shader還支持了Ramp貼圖來(lái)對(duì)光照進(jìn)行映射涩维⊙晷眨可以自己制作不同的Ramp貼圖來(lái)實(shí)現(xiàn)想要的效果。適當(dāng)采用一些漸變也有著反鋸齒的效果(下圖中間)激挪。
??其實(shí)這也是非吵浇疲基礎(chǔ)的操作锋叨,在Unity官方的Surface Shader Custom Lighting Example里就有示例垄分。
五彩斑斕的黑
??暗部如果只是純黑色就顯得很悶了,現(xiàn)在日系插畫都有著很漂亮的暗部顏色娃磺,所以增加了對(duì)暗部進(jìn)行著色的功能薄湿,混色算法采用了PS圖層混合模式的“濾色”模式。
Cel貼圖
??研究公司項(xiàng)目里的角色渲染時(shí)偷卧,發(fā)現(xiàn)存在一張被廣泛的使用的被稱為Cel的貼圖豺瘤,用來(lái)控制陰影形狀,有點(diǎn)法線貼圖的意思听诸。嘗試反推了該貼圖的用法坐求,使用后可以在頭發(fā)和衣服褶皺等地方看到明顯的效果。
邊緣光
這個(gè)很常見(jiàn)也很簡(jiǎn)單我就不多說(shuō)了晌梨,總之在NPR中也是蠻必要的一種效果桥嗤。
Stylized Highlight 風(fēng)格化高光
??一開始用傳統(tǒng)Blinn-Phong模型的高光算法,效果相當(dāng)惡心仔蝌,所幸找到了一個(gè)好用的輪子——風(fēng)格化的高光泛领。代碼是從這里來(lái)的,我翻譯了一下敛惊,然后添加了一個(gè)SpecMask貼圖的功能——對(duì)于不需要顯示高光的區(qū)域涂黑即可渊鞋。
??友情提示:此效果不適合面數(shù)很低的模型。
關(guān)于反鋸齒
??由于有外描邊這種細(xì)線的存在瞧挤,不進(jìn)行反鋸齒就很容易滿屏幕狗牙锡宋,分辨率越低越明顯,所以極力推薦采取一定的反鋸齒措施特恬。不管是MSAA還是后期處理的TAA或FXAA(在官方的PostProcessing包中就有)执俩,都會(huì)讓畫面觀感明顯變好,順便再配合一些此類渲染必備的Bloom效果鸵鸥,就可以獲得比較滿意的畫面了奠滑。
關(guān)于打光
??和一般的實(shí)時(shí)光照打光方式相同丹皱,推薦一個(gè)Directional Light即可。此外也會(huì)受環(huán)境光(Environment Lighting)影響宋税, 可以在Lighting頁(yè)面里調(diào)整摊崭。
完整代碼
特性大致就是以上這些了,下面是完整的Shader代碼杰赛。
// ----------一些參考----------
// http://www.ggxrd.com/Motomura_Junya_GuiltyGearXrd.pdf
// ----------------------------
Shader "Gypsum/Cel-Shading" {
Properties {
[Header(Culling)]
[Enum(UnityEngine.Rendering.CullMode)] _Cull ("Cull Mode", Float) = 2
[Space(5)]
[Header(Base Color)]
_Color ("Tint", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
[Header(Cel Shading Parameters)]
_ShadowColor ("Shadow Color", Color) = (0,0,0,1)
[Toggle(_ENABLE_RAMP)] _EnableRamp ("Enable Ramp", float) = 0.0
_RampTex ("Ramp Map", 2D) = "white" {}
_CelTex ("Cel Map", 2D) = "white" {}
_CelOffset ("Cel Offset", Range(-1,1)) = 0
[Space(5)]
[Header(Rim Light)]
[HDR] _RimColor ("Rim Color", Color) = (1,1,1,1)
_RimPower ("Rim Power", Range(1,64)) = 8
[Space(5)]
[Header(Outline)]
[KeywordEnum(REGULAR,VERTEXCOLOR,TANGENT,UV2)] _OutlineNormalMode ("Normal Mode", float) = 0.0
[Toggle(_OUTLINECONSTWIDTH)] _OutlineConstWidth ("Constant Width", float) = 0.0
_OutlineColor ("Color", Color) = (0, 0, 0, 1)
_OutlineWidth ("Width", Range(0,5)) = 1.0
[Toggle(_OUTLINEZSMOOTH)] _OutlineZSmooth ("Enable Z Correction", float) = 0.0
_ZSmooth ("Z Correction", Range(-3.0,3.0)) = -0.5
_Offset1 ("Z Offset", Float) = 0
// _Offset2 ("Z Offset 2", Float) = 0 //似乎沒(méi)什么作用所以沒(méi)有啟用
[Space(5)]
[Header(Specular)]
[Toggle(_ENABLE_SPECULAR)] _EnableSpecular ("Enable", float) = 0.0
[HDR] _SpecularColor ("Color", Color) = (1, 1, 1, 1)
_SpecularMask ("Mask", 2D) = "white" {}
_SpecularPower ("Shininess", Range(1, 100)) = 48
_SpecularSegment ("Segment", Range(0, 1)) = 0.9
}
Subshader {
Tags { "RenderType"="Opaque"}
CGPROGRAM
#pragma surface surf Cel addshadow
#pragma shader_feature _ENABLE_SPECULAR
#pragma shader_feature _ENABLE_RAMP
sampler1D _RampTex;
sampler2D _CelTex;
sampler2D _MainTex;
sampler2D _SpecularMask;
fixed _CelOffset;
fixed4 _ShadowColor;
fixed4 _Color;
fixed4 _RimColor;
half _RimPower;
half4 _SpecularColor;
half _SpecularPower;
fixed _SpecularSegment;
// ----------一些顏色混合函數(shù)----------
fixed Greyscale(fixed3 input)
{
return (input.r + input.g + input.b) / 3;
}
fixed3 Blend_Multiply(fixed3 color0, fixed3 color1)
{
return color0 * color1;
}
fixed3 Blend_Overlay(fixed3 color0, fixed3 color1)
{
if(Greyscale(color0) <= 0.5)
{
return 2 * color0 * color1;
}
else
{
return 1 - 2 * ((1 - color0) * (1 - color1));
}
}
fixed3 Blend_Screen(fixed3 color0, fixed3 color1)
{
return 1 - (1 - color0) * (1 - color1);
}
// ------------------------------------
struct Input {
float2 uv_MainTex;
float3 viewDir;
// fixed3 worldNormal;
// float3 worldPos;
};
// 自定義一個(gè)SurfaceOutput
struct SurfaceOutputCel
{
fixed3 Albedo;
fixed3 Emission;
float3 Normal;
fixed Alpha;
half2 UV; //在Lighting函數(shù)中貼圖就需要傳UV到Output中
// fixed3 WorldNormal;
// float3 WorldPos;
};
void surf(Input IN, inout SurfaceOutputCel o)
{
// Input to Output
o.UV = IN.uv_MainTex;
// o.WorldNormal = IN.worldNormal;
// o.WorldPos = IN.worldPos;
// Rim light
fixed rim = dot(o.Normal, IN.viewDir);
rim = (saturate(pow(1 - rim, _RimPower)));
fixed3 finalRim = rim * _RimColor.rgb * _RimColor.a;
o.Emission = finalRim;
// Base Color
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Alpha = c.a;
}
half4 LightingCel(SurfaceOutputCel s, half3 lightDir, half3 viewDir, half atten)
{
// ----------Stylized Highlights----------
// https://github.com/candycat1992/NPR_Lab
// ---------------------------------------
#ifdef _ENABLE_SPECULAR
fixed3 worldNormal = normalize(s.Normal);
fixed3 worldHalfDir = normalize(viewDir + lightDir);
fixed spec = max(0, dot(worldNormal, worldHalfDir));
spec = pow(spec, _SpecularPower);
fixed w = fwidth(spec);
if (spec < _SpecularSegment + w) {
spec = lerp(0, _SpecularSegment, smoothstep(_SpecularSegment - w, _SpecularSegment + w, spec));
} else {
spec = _SpecularSegment;
}
half3 specular = spec * _SpecularColor.rgb * tex2D(_SpecularMask, s.UV);
#else
fixed3 specular = 0;
#endif
// ----------------------------------------
// ----------Cel-Shading Lighting----------
// ----------------------------------------
half NdotL = dot(s.Normal, lightDir);
half cel = lerp(fixed3(1,1,1), saturate(Greyscale(tex2D(_CelTex, s.UV) + _CelOffset)), dot(lightDir,s.Normal));
#ifdef _ENABLE_RAMP
cel = tex1D(_RampTex, cel);
half ramp = tex1D(_RampTex, saturate(atten * NdotL) * 0.5 + 0.5);
half3 shadow = lerp(fixed3(1,1,1), Blend_Screen(fixed3(1,1,1) * saturate(ramp * cel), _ShadowColor.rgb), _ShadowColor.a);
#else
half3 shadow = lerp(fixed3(1,1,1), Blend_Screen(fixed3(1,1,1) * saturate(round(NdotL * atten * cel)), _ShadowColor.rgb), _ShadowColor.a);
#endif
half4 c;
c.rgb = Blend_Screen(shadow * s.Albedo * _LightColor0, specular);
c.a = s.Alpha;
return c;
}
ENDCG
// ----------Outline Pass----------
// https://www.videopoetics.com/tutorials/pixel-perfect-outline-shaders-unity/#building-the-classic-outline-shader
// https://assetstore.unity.com/packages/vfx/shaders/toony-colors-pro-2-8105
// --------------------------------
Pass {
Cull Front
Offset [_Offset1], 0 //[_Offset2]
CGPROGRAM
#include "UnityCG.cginc"
#pragma multi_compile _OUTLINENORMALMODE_REGULAR _OUTLINENORMALMODE_VERTEXCOLOR _OUTLINENORMALMODE_TANGENT _OUTLINENORMALMODE_UV2
#pragma shader_feature _OUTLINECONSTWIDTH
#pragma shader_feature _OUTLINEZSMOOTH
#pragma vertex Vertex
#pragma fragment Fragment
half _ZSmooth;
half _OutlineWidth;
half4 _OutlineColor;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 pos : SV_POSITION;
};
v2f Vertex(a2v v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
//Correct Z artefacts
#ifdef _OUTLINEZSMOOTH
float4 pos = float4(UnityObjectToViewPos(v.vertex), 1.0);
#ifdef _OUTLINENORMALMODE_VERTEXCOLOR
//Vertex Color for Normals
float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, (v.color.xyz*2) - 1);
#elif _OUTLINENORMALMODE_TANGENT
//Tangent for Normals
float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.tangent.xyz);
#elif _OUTLINENORMALMODE_UV2
//UV2 for Normals
float3 normal;
//unpack uv2
v.uv2.x = v.uv2.x * 255.0/16.0;
normal.x = floor(v.uv2.x) / 15.0;
normal.y = frac(v.uv2.x) * 16.0 / 15.0;
//get z
normal.z = v.uv2.y;
//transform
normal = mul( (float3x3)UNITY_MATRIX_IT_MV, normal*2-1);
#else
float3 normal = mul( (float3x3)UNITY_MATRIX_IT_MV, v.normal);
#endif
normal.z = -_ZSmooth;
#ifdef _OUTLINECONSTWIDTH
//Camera-independent outline size
float dist = distance(_WorldSpaceCameraPos, mul(unity_ObjectToWorld, v.vertex));
pos = pos + float4(normalize(normal),0) * _OutlineWidth * 0.01 * dist;
#else
pos = pos + float4(normalize(normal),0) * _OutlineWidth * 0.01;
#endif
#else
#ifdef _OUTLINENORMALMODE_VERTEXCOLOR
//Vertex Color for Normals
float3 normal = (v.color.xyz*2) - 1;
#elif _OUTLINENORMALMODE_TANGENT
//Tangent for Normals
float3 normal = v.tangent.xyz;
#elif _OUTLINENORMALMODE_UV2
//UV2 for Normals
float3 n;
//unpack uv2
v.uv2.x = v.uv2.x * 255.0/16.0;
n.x = floor(v.uv2.x) / 15.0;
n.y = frac(v.uv2.x) * 16.0 / 15.0;
//get z
n.z = v.uv2.y;
//transform
n = n*2 - 1;
float3 normal = n;
#else
float3 normal = v.normal;
#endif
//Camera-independent outline size
#ifdef _OUTLINECONSTWIDTH
float dist = distance(_WorldSpaceCameraPos, mul(unity_ObjectToWorld, v.vertex));
float4 pos = float4(UnityObjectToViewPos(v.vertex + float4(normal, 0) * _OutlineWidth * 0.01 * dist), 1.0);
#else
float4 pos = float4(UnityObjectToViewPos(v.vertex + float4(normal, 0) * _OutlineWidth * 0.01), 1.0);
#endif
#endif
o.pos = mul(UNITY_MATRIX_P, pos);
return o;
}
float4 Fragment (v2f IN) : COLOR
{
return _OutlineColor;
}
ENDCG
}
}
}
結(jié)語(yǔ)
??之前第一次看《罪惡裝備Xrd》藝術(shù)風(fēng)格講解的時(shí)候呢簸,有種驚為天人的感覺(jué),就一直想著自己什么時(shí)候也可以試著搞一下這類Shader乏屯。其實(shí)NPR都是一些很老的技術(shù)根时,好幾年前就可以做到了,只是它的難點(diǎn)從來(lái)就不是技術(shù)辰晕。
??PPT里有幾個(gè)點(diǎn)我認(rèn)為講得非常好:
不只是一個(gè)Shader就完事蛤迎,而是要構(gòu)建整個(gè)工作流。(Not just a shader, but a whole workflow)
??他們的人物動(dòng)畫每一幀都是手K含友,而且不做補(bǔ)間替裆,是為了追求“有限動(dòng)畫”的感覺(jué)。并且動(dòng)畫的每一幀都會(huì)對(duì)光照做針對(duì)性調(diào)整窘问,還充斥著大量的形變縮放辆童,以營(yíng)造日式動(dòng)畫類似“金田系”作畫的夸張透視效果。最后游戲能呈現(xiàn)出這樣幾乎沒(méi)有破綻的2D效果惠赫,巨大的美術(shù)工作量的功不可沒(méi)把鉴。試圖用一個(gè)Shader就想讓自己的游戲達(dá)到完美的風(fēng)格化渲染效果,無(wú)疑是天真的儿咱。讓美術(shù)決定效果庭砍,而不是數(shù)學(xué)公式。(Let the artist decide, not the math)
??確實(shí)有時(shí)候就會(huì)碰到這種情況——這個(gè)公式看起來(lái)更正確一點(diǎn)概疆,但是效果很微妙逗威;那種算法看起來(lái)很莫名其妙,但是效果很棒岔冀。所以該用哪種凯旭?可能大部分時(shí)候我們只需要表象正確就可以了,畢竟做游戲就少不了Trick使套,沒(méi)必要一味的追求“正確”吧罐呼。
??以上是一些個(gè)人的小小感想。希望本文對(duì)你有用侦高,再見(jiàn)嫉柴。