前言
【本文持续更新中】
终于把一直想做一做的仿原神渲染做了一下。
原神出来也有段时间了,各路大佬的逆向早就做完了,所以最近做的其实复刻大佬们的工程,难度并不大。
废话不多说,先看效果。
Unity
UE
(UE的边缘光老是闪就关了)
两个版本都没有加上雾效,泛光之间的后处理效果,本篇随笔也不会讲述相关内容,有能力的可以自己加上,效果应该会上一个档次
UE最近一段时间才开始用起来,材质蓝图比我想象的要简单一些,但是UE的PBR渲染几乎是高度封装的,导致几乎相同的流程UE会和Unity效果差异很明显。
这次的UE版本效果也就差强人意吧,后续继续做UE项目的话可能会改进。
前言
本文旨在分析原神在“角色渲染”这一方面所运用到的一些方法,并且几乎不涉及间接光照,并没有一个可用的风格化管线,仅仅是一些trick思路,在日后做一些风格化渲染上可以借鉴学习。
复刻差不多是在去年这个时候完成的,当时把代码贴上去说以后补上详细说明过程,但是一直没做。趁此机会复盘一下实现流程,然后就像上面说的,以后再尝试实现一套可用的风格化管线。
实现流程
资产
贴图模型网上应该比较轻松能找到,本着分享的原则,这里给出我的途径。
模之屋官方模型:https://www.aplaybox.com/u/680828836
原神官方会放出官方模型方便MMD二创,大部分模型也都会附带贴图,但特殊贴图可能没有。这个大概率不会是游戏内实际使用的模型,但是几乎不影响使用。
民间解包:
GenshinTextures/Texture2D/Avatar at master · TGSRedStone/GenshinTextures · GitHub
这个是著名的原神贴图解包版本,但好像很久没有更新了,这个上面可以扒到所有需要的贴图。
自食其力:抓帧贴图
现在用Renderdoc+mumu模拟器应该很容易能抓到原神,IntelGPA也是一个选择,但是我没成功过。另外还有一个感觉用的人不多,Snapdragon Profiler,用来抓手机的,感觉比较好用。
最近绝区零公测了,发现抓出来都是空帧,被米家算计了。找了个测试版本抓了一下,放出来当做示例。
Matcap
Material Capture,本质是一张贴图,上面提前烘焙好了材质信息,当我们需要的时候直接采样用。这是一个非常廉价的,不需要任何光照信息就可以获得一个看上去好像像那么回事的效果。
在风格化渲染中,大部分光照都是假的,通过matcap可以直接赋予角色一些看起来对的风格化的信息。比如伪高光,伪金属光泽,皮肤质感等。
在原神中,这张图的效果没有那么明显,对于皮肤和非皮肤分别使用下面两张图:
可以看到相比烘焙出来的贴图,这两张贴图的信息非常少。
看下效果:
得到了一个像是阴影一样的效果。
使用也非常简单,只需要根据视角空间法线采样就可以了。
//MatcapUV
float3 ViewDir_CameraSpace = mul(unity_WorldToCamera, i.worldNormal);
ViewDir_CameraSpace = ViewDir_CameraSpace * 0.5 + 0.5;
float2 MatcapUV = ViewDir_CameraSpace.xy;
//ToonColor SkinColor
float3 ToonColor = tex2D(_ToonTex, MatcapUV);
float3 SkinColor = tex2D(_SkinTex, MatcapUV);
ILM
这张图我看到有lightmap和“魔法图”两种叫法,但其实际用途应该是和光照贴图没有任何关系的。
ILM由四个通道组成:
R:高光mask、G:AO、B:高光强度、A:Ramp阈值
主要的作用是通过贴图直接赋予材质属性,可以在贴图上直接把高光形状,金属部分形状直接画出来,然后A通道配合Ramp图控制风格化过渡。
这个图通常长下面这个样子:
R通道,值很大的部分就是金属部分,更详细的强度信息被储存在B通道中。G通道能够很明显看出是AO,比如衣领的遮挡带来的死黑区域,还有一些边缘过渡。
Ramp
一般会有两条Ramp图,暖色和冷色分别对应白天和黑夜,也就是说当环境光发生变化时,角色光照只会切换Ramp图,并不会计算环境光照。这里的这张,一张图算两张,白天五行,夜晚五行。
Ramp是风格化渲染的重要部分,风格化明显的渲染在明暗过渡部分会做出硬边缘效果或者分层的效果,
通过采样Ramp图可以得到我们想要的过渡效果。
通常使用半兰伯特进行采样
tex2d(_RampTex, float2(HalfLambert, 0.5));
因为半兰伯特的范围是(0,1),然后在(0,1)之间随便取一个值即可。随便取一个值是因为我们平时用的Ramp基本都是X轴过渡,Y轴通常没有信息,理论上只需要一个像素就够了。
而对于原神的这张图,Y轴10个像素分别是10行Ramp,我们可以采样一张图得出多种过渡。至于该采样哪行,这个信息被储存在ILM的A通道中,我们扔到PS中可以看每个灰度的阈值是多少,就可以通过灰度值将这些部分分开。
代码如下:
//NightRamp_V
float ILM_Alpha_0 = 0.15;
float ILM_Alpha_1 = 0.40;
float ILM_Alpha_2 = 0.60;
float ILM_Alpha_3 = 0.85;
float ILM_Alpha_4 = 1.0;
float ILM_Value_0 = 1.0;
float ILM_Value_1 = 4.0;
float ILM_Value_2 = 3.0;
float ILM_Value_3 = 5.0;
float ILM_Value_4 = 2.0;
ILM_Value_0 = 0.55 - ILM_Value_0 / 10;
ILM_Value_1 = 0.55 - ILM_Value_1 / 10;
ILM_Value_2 = 0.55 - ILM_Value_2 / 10;
ILM_Value_3 = 0.55 - ILM_Value_3 / 10;
ILM_Value_4 = 0.55 - ILM_Value_4 / 10;
float NightRamp_V = lerp(ILM_Value_4, ILM_Value_3, step(ILM_A, ILM_Alpha_3));
NightRamp_V = lerp(NightRamp_V, ILM_Value_2, step(ILM_A, ILM_Alpha_2));
NightRamp_V = lerp(NightRamp_V, ILM_Value_1, step(ILM_A, ILM_Alpha_1));
NightRamp_V = lerp(NightRamp_V, ILM_Value_0, step(ILM_A, ILM_Alpha_0));
//DayRamp_V
float DayRamp_V = NightRamp_V + 0.5;
只需要算白天或者晚上就行,然后加减0.5。
然后根据光线方向进行一个简单的差值就是正常的过渡效果。但是本篇没有做环境光照,所以如果只复刻角色渲染的话,大概率会看到场景一片死黑。
同时这张图的过渡范围几乎全在最右边,而一般的Ramp过渡都在中间,因为半兰伯特的阴影过渡在0.5的位置。对于这一点,我们可以理解为,这张图仅包含了过渡部分的信息,最右边即为阴影边界,所有的受光面全部采样采到最右端,是没有过渡的颜色,而我们需要的过渡部分仅在背光面。说实话,这样做我觉得谈不上有太多优化吧,只能算是一种制作方式。
修改采样方法也很简单:
float HalfLambert_Ramp = smoothstep(0.0, 0.5, HalfLambert);
这样超出0.5的部分就会变成1.0,全部采样到最右端。
Ramp效果:
然后加上AO,AO原本长这个样子,但我们不会让这边一片死黑,所以采用Ramp图最左端的颜色作为AO颜色。
高光
通过ILM的R通道判断是否为金属
//IsMetallic
float IsMetallic = step(0.95, ILM_R);
通过一张MetalTex,乘上BaseColor得到金属颜色。
//Metallic
float3 Metallic = lerp(0, tex2D(_MetalTex, MatcapUV).r * BaseColor, IsMetallic);
这张图也没什么高深之处,根据Matcap原理我们可以知道,这张图就是把视角空间的法线分层,效果上就是正对视角的高光最亮,然后两边更暗。
高光使用BlinnPhong,乘上ILM的B通道得到高光。
//BlinnPhong
float3 HalfDir = normalize(ViewDir + LightDir);
float NOH = dot(i.worldNormal, HalfDir);
float BlinnPhong = step(0, NOL) * pow(max(0, NOH), _Gloss);
边缘光
这里用的边缘光虽然不是菲涅尔,但是原理也很简单,一般称作屏幕空间等距边缘光。
本质上我们是想要获取从物体的边缘开始的某个范围,而确定物体轮廓经常会用到深度缓冲,物体边缘的深度与场景的深度差别很大,这时就能确定物体边缘。
程序上,我们将屏幕空间UV沿视角空间法线方向偏移一段,然后采样偏移后的UV,比较偏移前后得到的深度,根据设定好的阈值进行边缘光差值即可。
代码如下:
float rawDepth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, i.scrPos);
float linearDepth = LinearEyeDepth(rawDepth);
float Offset = lerp(-1, 1, step(0, viewNormal.x)) * rimOffset / _ScreenParams.x;
float4 screenOffset = float4(Offset, 0, 0, 0);
float offsetDepth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, i.scrPos + screenOffset);
float offsetLinearDepth = LinearEyeDepth(offsetDepth);
float rim = saturate(offsetLinearDepth - linearDepth);
rim = step(rimThreshold, rim) * clamp(rim * rimStrength, 0, rimMax);
SDF
应用在面部阴影的方案应该是米哈游首创吧?如今已经成为一众二次元游戏的标准流程了。
这张图本质上是数张面部阴影图叠起来的,每一张阴影图都只有0和1,根据设定好的阈值选择用哪张面部阴影图。
肯定是用光照方向和阈值比较了,我们从外部获取到角色的面部朝向,然后和光照方向点积就可以得到角度,根据这个角度去和SDF比较,得到阴影图数据。
加俩物体获取朝向,然后获取相对位置,代码如下:
Vector3 forwardVector = HeadForward.position - HeadTransform.position;
Vector3 rightVector = HeadRight.position - HeadTransform.position;
forwardVector = forwardVector.normalized;
rightVector = rightVector.normalized;
FaceMaterial.SetVector("_ForwardVector", forwardVector);
FaceMaterial.SetVector("_RightVector", rightVector);
SDF代码如下:
注意当光源从左边转到右边时,采样的UV方向要反转
//SDF
float3 UpVector = cross(_ForwardVector, _RightVector);
float3 LpU = dot(LightDir, UpVector) / pow(length(UpVector), 2) * UpVector;
float3 LpHeadHorizon = LightDir - LpU;
float value = acos(dot(normalize(LpHeadHorizon), normalize(_RightVector))) / UNITY_PI;
float exposeRight = step(value, 0.5);
float value_R = pow(1 - value * 2, 3);
float value_L = pow(value * 2 - 1, 3);
float mixValue = lerp(value_L, value_R, exposeRight);
float sdfRembrandtLeft = tex2D(_SDF_Tex, float2(1 - i.uv.x, i.uv.y)).r;
float sdfRembrandtRight = tex2D(_SDF_Tex, i.uv).r;
float mixSDF = lerp(sdfRembrandtRight, sdfRembrandtLeft, exposeRight);
float SDF = step(mixValue, mixSDF);
SDF = lerp(0, SDF, step(0, dot(normalize(LpHeadHorizon), normalize(_ForwardVector))));
float4 FaceShadowTex = tex2D(_FaceShadow_Tex, i.uv);
SDF *= FaceShadowTex.g;
SDF = lerp(SDF, 1, FaceShadowTex.a);
效果如下:
描边
描边对于风格化渲染来说可谓之重中之重,加上描边一看就很有风格的样子(
这里的描边方法其实也比较传统,采用背面法线外扩的方法,分两个Pass分别渲染正背面。
出来效果会发现描边断裂成一段一段的很不好看,所以需要将法线平滑然后存起来用作描边。
平滑工具使用:https://www.bilibili.com/read/cv24126974/
我对于Unity的Mesh操作还是不熟,这里使用了别人现成的脚本,大体思路就是,对于每个顶点,根据其在三角形中连接的两条边的角度算出一个权重,然后遍历所有的顶点,算出权重之和,然后每个顶点的法线根据自己的权重乘上这个比例。
描边的宽度会随摄像机远近变化,这是因为我们在NDC空间下进行法线外扩的操作,这个时候已经经过了透视除法,避免这种情况只需要把w分量乘回来就可以了。
代码如下:
float4 pos = UnityObjectToClipPos(v.vertex);
float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.tangent.xyz);
//将法线变换到NDC空间,乘以w,消除透视影响
float3 ndcNormal = normalize(TransformViewToProjection(viewNormal.xyz)) * pos.w;
float4 nearUpperRight = mul(unity_CameraInvProjection, float4(1, 1, UNITY_NEAR_CLIP_VALUE, _ProjectionParams.y));
//求得屏幕宽高比
float aspect = abs(nearUpperRight.y / nearUpperRight.x);
ndcNormal.x *= aspect;
pos.xy += 0.001 * clamp(_OutlineOffset * ndcNormal.xy, -50, 50);
o.position = pos;
做到这里的时候,发现角色五官有很奇怪的描边,理想情况这个地方的描边从正面应该不可见。临时扣了一块mask把描边删了,但会导致侧面描边不可见。
差不多扣成这个样子,有点丑(扣了好久没扣对位置)
简单mask一下:
float FaceMask = tex2Dlod(_Face_Mask_Tex, float4(v.uv, 0, 0)).a;
_OutlineOffset = lerp(0, _OutlineOffset, FaceMask);
描边效果如下:
最终效果如下:
总结
能够发现其实光角色渲染这些东西也不是很有技术含量的,其实就原神这套贴图随便拿个光照模型贴上去效果都会不错的,所以风格化主要还是看美术的审美。不过毕竟是原神,去年的我还什么都不懂,对于这种比较容易出效果的复刻还是有一定成就感的,别人问起来风格化渲染的一些点自己好歹能听懂两句。未来可能会尝试做一套可以用的风格化管线吧,而且越学越觉得,理论技术,甚至能够复刻的一些技术,都是要能够落地才能发挥价值的。原神这套东西厉害在他有一套标准的资产生产流程,仅仅是复刻某些技术的话难免变成玩具。未来会尽量以能够落地为原则继续学习吧。
同文博客:https://www.cnblogs.com/IslandZ/p/17608128.html
参考文献
[1] 原神Shader渲染还原解析. https://zhuanlan.zhihu.com/p/435005339
[2] 原神角色渲染Shader分析还原. https://zhuanlan.zhihu.com/p/360229590
[3] Unity描边平滑法线存UV7脚本. https://www.bilibili.com/read/cv24126974/
源码
1 Shader "Custom/Cel_Base" 2 { 3 Properties 4 { 5 _AmbientColor ("Ambient Color", Color) = (1,1,1,1) 6 _AmbientFac("Ambient Fac", Range(0, 1)) = 0 7 _SphereTex("Sphere Tex", 2D) = "white" {} 8 _SphereTexFac("Sphere Fac", Range(0, 1)) = 0 9 _OutlineOffset("Outline Offset", Float) = 0.01 10 _OutlineShadowColor("Outline Shadow", Color) = (1, 1, 1, 1) 11 _BaseTex ("Base Tex", 2D) = "white" {} 12 [Toggle(_True)]_IsSkin("Is Skin", Float) = 1 13 _ToonTex ("Toon Tex", 2D) = "white" {} 14 _SkinTex ("Skin Tex", 2D) = "white" {} 15 _MatcapFac("Matcap Fac", Range(0, 1)) = 0 16 _MetalTex("Metal Tex", 2D) = "white" {} 17 _ILM_Tex("ILM Tex", 2D) = "white" {} 18 _ILM_R_TMP("ILM R", 2D) = "white" {} 19 [Toggle(_True)]_IsEye("Is Eye", Float) = 1 20 _Eye_Mask_Tex("Eye Mask Tex", 2D) = "white" {} 21 _Gloss("Gloss", Float) = 50 22 _KsNonMetallic("Non Metallic", Float) = 1 23 _KsMetallic("Metallic", Float) = 1 24 _KsHairMetallic("Hair Metallic", Float) = 1 25 [Toggle(_True)]_IsHair("Is Hair", Float) = 1 26 _RampTex ("Ramp Tex", 2D) = "white" {} 27 _ShadowColor ("Shadow Color", Color) = (1, 1, 1, 1) 28 _RimColor ("Rim Color", Color) = (1, 1, 1, 1) 29 _RimFac ("Rim Fac", Float) = 0 30 31 } 32 SubShader 33 { 34 Tags {"LightMode" = "ForwardBase" "RenderType" = "Opaque" } 35 Pass 36 { 37 Cull Off 38 CGPROGRAM 39 #include "UnityCG.cginc" 40 #include "Lighting.cginc" 41 #include "AutoLight.cginc" 42 43 #pragma vertex vert 44 #pragma fragment frag 45 #pragma multi_compile_fwdbase 46 47 sampler2D _CameraDepthTexture; 48 49 sampler2D _BaseTex; 50 sampler2D _ToonTex; 51 sampler2D _RampTex; 52 sampler2D _SkinTex; 53 sampler2D _SphereTex; 54 sampler2D _ILM_Tex; 55 sampler2D _ILM_R_TMP; 56 sampler2D _MetalTex; 57 float4 _AmbientColor; 58 float4 _ShadowColor; 59 float4 _RimColor; 60 float _MatcapFac; 61 float _AmbientFac; 62 float _SphereTexFac; 63 float _Gloss; 64 float _KsNonMetallic; 65 float _KsMetallic; 66 float _KsHairMetallic; 67 float _IsHair; 68 float _IsSkin; 69 float _RimFac; 70 71 struct a2v 72 { 73 float4 vertex : POSITION; 74 float3 normal : NORMAL; 75 float2 texcoord : TEXCOORD0; 76 }; 77 78 struct v2f 79 { 80 float4 pos : SV_POSITION; 81 float4 scrPos : TEXCOORD3; 82 float4 worldPos : TEXCOORD2; 83 float3 worldNormal : TEXCOORD1; 84 float2 uv : TEXCOORD0; 85 SHADOW_COORDS(4) 86 }; 87 88 v2f vert(a2v v) 89 { 90 v2f o; 91 o.pos = UnityObjectToClipPos(v.vertex); 92 o.scrPos = ComputeScreenPos(o.pos); 93 o.uv = v.texcoord; 94 o.worldPos = mul(unity_ObjectToWorld, v.vertex); 95 o.worldNormal = mul(unity_ObjectToWorld, v.normal); 96 TRANSFER_SHADOW(o); 97 return o; 98 } 99 100 float4 frag(v2f i) : SV_Target 101 { 102 UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); 103 //Context 104 float3 LightDir = normalize(_WorldSpaceLightPos0); 105 float3 ViewDir = normalize(_WorldSpaceCameraPos); 106 float3 viewNormal = mul(unity_WorldToCamera, i.worldNormal); 107 float NOL = dot(i.worldNormal, LightDir); 108 float NOV = dot(i.worldNormal, ViewDir); 109 110 //Lambert HalfLambert 111 float Lambert = max(0, NOL); 112 float HalfLambert = pow((NOL * 0.5 + 0.5), 1); 113 114 //Ramp采样用 HalfLambert_Ramp 115 116 float HalfLambert_Ramp = smoothstep(0.0, 0.5, HalfLambert); 117 118 //DarkRamp平滑用 LambertStep 119 float LambertStep = smoothstep(0.423, 0.465, HalfLambert); 120 121 //BaseColor 122 float4 BaseTexColor = tex2D(_BaseTex, i.uv); 123 //return float4(BaseTexColor.rgb, 1); 124 125 //MatcapUV 126 float3 ViewDir_CameraSpace = mul(unity_WorldToCamera, i.worldNormal); 127 ViewDir_CameraSpace = ViewDir_CameraSpace * 0.5 + 0.5; 128 float2 MatcapUV = ViewDir_CameraSpace.xy; 129 130 //ToonColor SkinColor 131 float3 ToonColor = tex2D(_ToonTex, MatcapUV); 132 float3 SkinColor = tex2D(_SkinTex, MatcapUV); 133 134 //MatcapColor 135 float3 MatcapColor; 136 if (_IsSkin == 1) MatcapColor = SkinColor; 137 else MatcapColor = ToonColor; 138 139 //BaseColor 140 float3 BaseColor = lerp(BaseTexColor, BaseTexColor * MatcapColor, _MatcapFac); 141 BaseColor = lerp(BaseColor, BaseColor * _AmbientColor, _AmbientFac); 142 float3 SphereColor = tex2D(_SphereTex, MatcapUV); 143 BaseColor = lerp(BaseColor, BaseColor * SphereColor, _SphereTexFac); 144 145 //ILM 146 float4 ILM_RGBA = tex2D(_ILM_Tex, i.uv); 147 float ILM_R = ILM_RGBA.x; 148 float ILM_G = ILM_RGBA.y; 149 float ILM_B = ILM_RGBA.z; 150 float ILM_A = ILM_RGBA.w; 151 152 //NightRamp_V 153 float ILM_Alpha_0 = 0.15; 154 float ILM_Alpha_1 = 0.40; 155 float ILM_Alpha_2 = 0.60; 156 float ILM_Alpha_3 = 0.85; 157 float ILM_Alpha_4 = 1.0; 158 float ILM_Value_0 = 1.0; 159 float ILM_Value_1 = 4.0; 160 float ILM_Value_2 = 3.0; 161 float ILM_Value_3 = 5.0; 162 float ILM_Value_4 = 2.0; 163 ILM_Value_0 = 0.55 - ILM_Value_0 / 10; 164 ILM_Value_1 = 0.55 - ILM_Value_1 / 10; 165 ILM_Value_2 = 0.55 - ILM_Value_2 / 10; 166 ILM_Value_3 = 0.55 - ILM_Value_3 / 10; 167 ILM_Value_4 = 0.55 - ILM_Value_4 / 10; 168 float NightRamp_V = lerp(ILM_Value_4, ILM_Value_3, step(ILM_A, ILM_Alpha_3)); 169 NightRamp_V = lerp(NightRamp_V, ILM_Value_2, step(ILM_A, ILM_Alpha_2)); 170 NightRamp_V = lerp(NightRamp_V, ILM_Value_1, step(ILM_A, ILM_Alpha_1)); 171 NightRamp_V = lerp(NightRamp_V, ILM_Value_0, step(ILM_A, ILM_Alpha_0)); 172 173 //DayRamp_V 174 float DayRamp_V = NightRamp_V + 0.5; 175 176 //IsDay 177 float IsDay = (LightDir.y + 1) / 2; 178 179 //RampColor 180 //采样Ramp时,HalfLmabert * AO可以得到带 AO的 RampColor 181 float3 DayRampColor = tex2D(_RampTex, float2(HalfLambert_Ramp, DayRamp_V)); 182 float3 DayDarkRampColor = tex2D(_RampTex, float2(0.003, DayRamp_V)); 183 float3 NightRampColor = tex2D(_RampTex, float2(HalfLambert_Ramp, NightRamp_V)); 184 float3 NightDarkRampColor = tex2D(_RampTex, float2(0.003, NightRamp_V)); 185 float3 RampColor = lerp(NightRampColor, DayRampColor, IsDay); 186 float3 DarkRampColor = lerp(NightDarkRampColor, DayDarkRampColor, IsDay); 187 188 //Diffuse 189 //使用 LambertStep平滑 190 float3 Diffuse = lerp(BaseColor * RampColor * _ShadowColor, BaseColor, LambertStep); 191 Diffuse = lerp(BaseColor * DarkRampColor * _ShadowColor, Diffuse, 1); 192 //使用 ILM_G增加AO,同时AO使用Ramp图最左侧颜色 193 //ILM_G范围为 0-0.5,大于等于0.5的部分为非AO部分,故映射为 0-1 194 Diffuse = lerp(BaseColor * DarkRampColor * _ShadowColor, Diffuse, ILM_G * 2); 195 196 197 //BlinnPhong 198 float3 HalfDir = normalize(ViewDir + LightDir); 199 float NOH = dot(i.worldNormal, HalfDir); 200 float BlinnPhong = step(0, NOL) * pow(max(0, NOH), _Gloss); 201 202 //IsMetallic 203 float IsMetallic = step(0.95, ILM_R); 204 float ILM_Hair_R = tex2D(_ILM_R_TMP, i.uv).r; 205 206 //Specular 207 float NonMetallic_Specular = step(1.04 - BlinnPhong, ILM_B) * ILM_R * _KsNonMetallic; 208 float3 HairMetallic_Specular = 0; 209 if (_IsHair == 1) HairMetallic_Specular = ILM_B * LambertStep * BaseColor * ILM_Hair_R * _KsHairMetallic; 210 float3 KsMetallic_Specular = BlinnPhong * ILM_B * (LambertStep * 0.8 + 0.2) * BaseColor * _KsMetallic; 211 float3 Specular = lerp(NonMetallic_Specular + HairMetallic_Specular, KsMetallic_Specular, IsMetallic); 212 213 //Metallic 214 float3 Metallic = lerp(0, tex2D(_MetalTex, MatcapUV).r * BaseColor, IsMetallic); 215 216 //Albedo 217 float3 Albedo = Diffuse + Specular + Metallic; 218 219 float rimOffset = 13; 220 float rimThreshold = 0.08; 221 float rimStrength = 0.6; 222 float rimMax = 0.3; 223 224 float rawDepth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, i.scrPos); 225 226 float linearDepth = LinearEyeDepth(rawDepth); 227 float Offset = lerp(-1, 1, step(0, viewNormal.x)) * rimOffset / _ScreenParams.x; 228 //float Offset = lerp(-1, 1, step(0, viewNormal.x)) * rimOffset / _ScreenParams.x / max(1, pow(linearDepth, 0.5)); 229 float4 screenOffset = float4(Offset, 0, 0, 0); 230 float offsetDepth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, i.scrPos + screenOffset); 231 float offsetLinearDepth = LinearEyeDepth(offsetDepth); 232 233 float rim = saturate(offsetLinearDepth - linearDepth); 234 rim = step(rimThreshold, rim) * clamp(rim * rimStrength, 0, rimMax); 235 236 float fresnelPower = 6; 237 float fresnelClamp = 0.8; 238 float fresnel = 1 - saturate(NOV); 239 fresnel = pow(fresnel, fresnelPower); 240 fresnel = fresnel * fresnelClamp + (1 - fresnelClamp); 241 242 Albedo = 1 - (1 - rim * fresnel * _RimColor * BaseTexColor * _RimFac) * (1 - Albedo); 243 244 float4 FinalColor = float4(Albedo, BaseTexColor.a); 245 return FinalColor; 246 } 247 248 ENDCG 249 } 250 251 Pass 252 { 253 Name "DrawOutline" 254 Tags {"RenderPipeline" = "UniversalPipeline" "RenderType" = "Opaque"} 255 Cull Front 256 257 CGPROGRAM 258 259 #pragma vertex vert 260 #pragma fragment frag 261 #pragma multi_compile_fog 262 #include "UnityCG.cginc" 263 264 sampler2D _BaseTex; 265 sampler2D _ILM_Tex; 266 sampler2D _RampTex; 267 sampler2D _Eye_Mask_Tex; 268 269 float4 _OutlineShadowColor; 270 float _OutlineOffset; 271 float _IsEye;323 324 struct a2v 325 { 326 float4 vertex : POSITION; 327 float3 normal : NORMAL; 328 float4 tangent : TANGENT; 329 float2 uv : TEXCOORD0; 330 float4 uv7 : TEXCOORD7; 331 }; 332 333 struct v2f 334 { 335 float4 position : SV_POSITION; 336 float2 uv : TEXCOORD0; 337 }; 338 339 v2f vert(a2v v) 340 { 341 v2f o; 342 343 /*float3 worldPos = mul(UNITY_MATRIX_M, v.vertex).xyz; 344 float3 worldNormal = UnityObjectToWorldNormal(v.normal); 345 float3 worldTangent = UnityObjectToWorldDir(v.tangent); 346 float3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; 347 float3 ViewPos = mul((float3x3)UNITY_MATRIX_IT_MV, v.vertex); 348 349 float3x3 tbn = float3x3(worldTangent, worldBinormal, worldNormal); 350 float3 normalWS = mul(v.normal.rgb, tbn); 351 352 float EyeMask = 1; 353 if (_IsEye == 1) EyeMask = tex2Dlod(_Eye_Mask_Tex, float4(v.uv, 0, 0)).a; 354 _OutlineOffset = lerp(0, _OutlineOffset, EyeMask); 355 356 float3 positionWS = TransformPositionWSToOutlinePositionWS( 357 worldPos, ViewPos.z, normalWS); 358 359 o.position = UnityObjectToClipPos(positionWS); 360 o.uv = v.uv;*/ 361 362 float4 pos = UnityObjectToClipPos(v.vertex); 363 //float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.uv7.xyz); 364 float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.tangent.xyz); 365 float3 ndcNormal = normalize(TransformViewToProjection(viewNormal.xyz)) * pos.w;//将法线变换到NDC空间 366 float4 nearUpperRight = mul(unity_CameraInvProjection, float4(1, 1, UNITY_NEAR_CLIP_VALUE, _ProjectionParams.y));//将近裁剪面右上角的位置的顶点变换到观察空间 367 float aspect = abs(nearUpperRight.y / nearUpperRight.x);//求得屏幕宽高比 368 ndcNormal.x *= aspect; 369 pos.xy += 0.001 * clamp(_OutlineOffset * ndcNormal.xy, -50, 50); 370 371 o.position = pos; 372 o.uv = v.uv; 373 374 return o; 375 } 376 float4 frag(v2f i) : SV_Target 377 { 378 379 //Context 380 float3 LightDir = normalize(_WorldSpaceLightPos0); 381 382 //BaseColor 383 float4 BaseTexColor = tex2D(_BaseTex, i.uv); 384 385 float4 ILM_RGBA = tex2D(_ILM_Tex, i.uv); 386 float ILM_A = ILM_RGBA.w; 387 388 //NightRamp_V 389 float ILM_Alpha_0 = 0.15; 390 float ILM_Alpha_1 = 0.40; 391 float ILM_Alpha_2 = 0.60; 392 float ILM_Alpha_3 = 0.85; 393 float ILM_Alpha_4 = 1.0; 394 float ILM_Value_0 = 1.0; 395 float ILM_Value_1 = 4.0; 396 float ILM_Value_2 = 3.0; 397 float ILM_Value_3 = 5.0; 398 float ILM_Value_4 = 2.0; 399 ILM_Value_0 = 0.55 - ILM_Value_0 / 10; 400 ILM_Value_1 = 0.55 - ILM_Value_1 / 10; 401 ILM_Value_2 = 0.55 - ILM_Value_2 / 10; 402 ILM_Value_3 = 0.55 - ILM_Value_3 / 10; 403 ILM_Value_4 = 0.55 - ILM_Value_4 / 10; 404 float NightRamp_V = lerp(ILM_Value_4, ILM_Value_3, step(ILM_A, ILM_Alpha_3)); 405 NightRamp_V = lerp(NightRamp_V, ILM_Value_2, step(ILM_A, ILM_Alpha_2)); 406 NightRamp_V = lerp(NightRamp_V, ILM_Value_1, step(ILM_A, ILM_Alpha_1)); 407 NightRamp_V = lerp(NightRamp_V, ILM_Value_0, step(ILM_A, ILM_Alpha_0)); 408 409 //DayRamp_V 410 float DayRamp_V = NightRamp_V + 0.5; 411 412 //IsDay 413 float IsDay = (LightDir.y + 1) / 2; 414 415 //RampColor 416 float3 DayDarkRampColor = tex2D(_RampTex, float2(0.003, DayRamp_V)); 417 float3 NightDarkRampColor = tex2D(_RampTex, float2(0.003, NightRamp_V)); 418 float3 DarkRampColor = lerp(NightDarkRampColor, DayDarkRampColor, IsDay); 419 420 421 return float4(BaseTexColor * DarkRampColor * _OutlineShadowColor, BaseTexColor.a); 422 } 423 ENDCG 424 } 425 } 426 FallBack "Diffuse" 427 }