第二课shader复现

第二课的shader.
SF有些实现好怪- -
翡翠效果
已经是我的极限了,美术牛
实现流程大概是这样:
首先进行基本的Lambert构建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| Shader "ZhuangDong/Lec2/DoubleSpecTamaMaterial" { Properties { _Diffuse("Diffuse Color", Color) = (1,1,1,1) } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag
#include "UnityCG.cginc" #include "Lighting.cginc"
struct v2f { float4 vertex : SV_POSITION; float3 worldPos : TEXCOORD0; float3 worldNormal : TEXCOORD1; float2 uv : TEXCOORD2; };
fixed4 _Diffuse;
v2f vert (appdata_base v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal = UnityObjectToWorldNormal(normalize(v.normal)); o.uv = TRANSFORM_TEX(v.texcoord, _RampTex); return o; }
fixed4 frag (v2f i) : SV_Target { float3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); float diffuse = 0.5 * dot(i.worldNormal, lightDir) + 0.5; return fixed4(diffuse,diffuse,diffuse, 1.0); } ENDCG } } }
|
然后根据half lambert的计算结果对RampTex进行采样。RampTex是随便拉出来的,长这个样子:
采样代码:
1
| fixed3 rampColor = tex2D(_RampTex, float2(1-diffuse, .5)).rgb;
|
接下来考虑高光的遮罩制作。先对光照方向进行偏移:
1 2
| float3 lightOffset1 = normalize(float3(lightDir.x + _OffsetValue1, lightDir.y, lightDir.z)); float3 lightOffset2 = normalize(float3(lightDir.x, lightDir.y + _OffsetValue2, lightDir.z));
|
这样得到两个偏移以后的光照,进而在此基础上进行lambert
1 2
| float diff1 = max(0, dot(i.worldNormal, lightOffset1)); float diff2 = max(0, dot(i.worldNormal, lightOffset2));
|
对它进行一定的采样。我们遮罩的要求是如果$diff>r$,$r$是某个域值,就为高光点,否则不是高光点。因此,使用step
函数,其中step(a,x)=a>x?1:0
1 2
| float spec1 = step(_SpecularThreshold, diff1); float spec2 = step(_SpecularThreshold, diff2);
|
得到两个高光之后,对二者取max,得到两个高光点
1
| float spec = max(spec1, spec2);
|
此时的遮罩如下
接下来要实现当spec是1的时候显示高光,否则显示原来的纹理颜色。这个操作可以用spec对原有颜色和现在的颜色进行lerp:
1
| fixed3 targetColor = lerp(rampColor, whiteColor, spec);
|
现在已经初具形状了,但仍然感觉不够晶莹剔透。所以我们引入fresnel term:
$$
fr = (1-\mathrm{v}\cdot\mathrm{n})^r
$$
当$\mathrm{v,n}$接近的时候,这个值接近0;否则接近1.也就是在边缘上,这个值接近1,中间接近0.那么根据这个值对原始颜色和白色进行插值,就可以得到一个发光的外表面。
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| Shader "ZhuangDong/Lec2/DoubleSpecTamaMaterial" { Properties { _Diffuse("Diffuse Color", Color) = (1,1,1,1) _RampTex("Ramp Texture", 2D) = "white" {} _OffsetValue1("Offset Value 1", float) = .01 _OffsetValue2("Offset Value 2", float) = .5 _SpecularThreshold("Specular Threshold", float) = .99 _FresnelPow("Fresnel Pow", float) = 3 } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag
#include "UnityCG.cginc" #include "Lighting.cginc"
struct v2f { float4 vertex : SV_POSITION; float3 worldPos : TEXCOORD0; float3 worldNormal : TEXCOORD1; float2 uv : TEXCOORD2; };
sampler2D _RampTex; float4 _RampTex_ST; float _OffsetValue1; float _OffsetValue2; float _SpecularThreshold; fixed4 _Diffuse; float _FresnelPow;
v2f vert (appdata_base v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal = UnityObjectToWorldNormal(normalize(v.normal)); o.uv = TRANSFORM_TEX(v.texcoord, _RampTex); return o; }
fixed4 frag (v2f i) : SV_Target { float3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); float3 lightOffset1 = normalize(float3(lightDir.x + _OffsetValue1, lightDir.y, lightDir.z)); float3 lightOffset2 = normalize(float3(lightDir.x, lightDir.y + _OffsetValue2, lightDir.z)); float diff1 = max(0, dot(i.worldNormal, lightOffset1)); float diff2 = max(0, dot(i.worldNormal, lightOffset2)); float spec1 = step(_SpecularThreshold, diff1); float spec2 = step(_SpecularThreshold, diff2); float spec = max(spec1, spec2); float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; float diffuse = 0.5 * dot(i.worldNormal, lightDir) + 0.5; fixed3 rampColor = tex2D(_RampTex, float2(1-diffuse, .5)).rgb; fixed3 whiteColor = fixed3(1, 1, 1); fixed3 targetColor = lerp(rampColor, whiteColor, spec); float3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); fixed fresnel = pow(1-dot(viewDir, i.worldNormal), _FresnelPow); fixed3 col = lerp(targetColor, whiteColor, fresnel); return fixed4(targetColor, 1.0); } ENDCG } } }
|
带子效果
这玩意的复现之路障碍重重…
首先的拦路虎就是,如何获取屏幕空间坐标?
有一个方法叫ComputeScreenPos
,它大概长这个样子:
1 2 3 4 5 6 7
| inline float4 ComputeScreenPos(float4 pos) { float4 o = ComputeNonStereoScreenPos(pos); #if defined(UNITY_SINGLE_PASS_STEREO) o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w); #endif return o; }
|
显然得不到什么有用信息。这个Single Pass Stereo Rendering
是一个VR的参数,具体可以见这里。进一步考察ComputeNonStereoScreenPos
这个函数:
1 2 3 4 5 6
| inline float4 ComputeNonStereoScreenPos(float4 pos) { float4 o = pos * 0.5f; o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w; o.zw = pos.zw; return o; }
|
首先要说明的是,这里传入的参数pos是经过MVP和NDC之后的坐标pos。
我们先抛开这里的_ProjectionParams
不谈。由于
$$
srcX = \frac{p_x/p_w + 1}{2} \cdot W
$$
这是因为$p_x \in [-p_w, p_w]$。而我们要求的纹理
$$
\frac{srcX}{W} = \frac{p_x/p_w + 1}{2}
$$
这正好是对应UV的$x$值。考虑下面的函数
$$
x = 0.5p_x + 0.5 p_w
$$
那么这个$x$实际上对应了UV的$x$乘上$p_w$。
_ProjectionParams
控制了透视投影的一些参量:
- $x$代表是否使用翻转投影矩阵,如果是则为$-1.0$,否则为$1.0$
- $y$为近平面,$z$为远平面,$w$为远平面的倒数
总而言之,这个函数的返回值是$[0, w]$的一个数。但我们往往需要一个$[0,1]$的数,否则无法采样。实际上,Unity提供了一个函数tex2Dproj
,这个函数是在投影的基础上进行采样,也就是
1 2 3
| tex2D(_Tex, float2(screenPos.xy / screenPos.w));
tex2Dproj(_Tex, screenPos);
|
如果直接这样采样,得到的结果大概是这样的:
先放一个代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| Shader "ZhuangDong/Lec2/ScreenBatch" { Properties { _BatchTex ("Texture", 2D) = "white" {} _TextureScale("Texture Scale", float) = 1000 } SubShader {
Pass {
CGPROGRAM #pragma vertex vert #pragma fragment frag
#include "UnityCG.cginc"
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; };
struct v2f { float4 screenPos : TEXCOORD1; float4 vertex : SV_POSITION; };
sampler2D _BatchTex; float4 _BatchTex_ST; float _TextureScale; sampler2D _RampTex; float4 _RampTex_ST;
v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.screenPos = ComputeScreenPos(o.vertex); return o; }
fixed4 frag (v2f i) : SV_Target { i.screenPos.xy = i.screenPos.xy * 10; return tex2Dproj(_BatchTex, i.screenPos); } ENDCG } } }
|
但这么做的问题在于,放大缩小之后,纹理大小并不发生改变,而这并不是我们想看到的。我们希望放大纹理也放大,缩小纹理也缩小,那这可以通过乘上一个深度来实现。所以下一个问题就是,深度怎么求?好在,Unity有宏COMPUTE_EYEDEPTH
来完成这项工作。它的代码也非常简单:
1
| #define COMPUTE_EYEDEPTH(o) o = -UnityObjectToViewPos( v.vertex ).z
|
之所以我们还要乘上一个负号,是因为观察空间是右手系。所以总的来说,求深度的方法是这样:
1 2 3
| o.screenPos = ComputeScreenPos(o.vertex); COMPUTE_EYEDEPTH(o.screenPos.z); float dep = max(0, o.screenPos.z - _ProjectionParams.g);
|
这里再插一个热知识。我们看Unity在UnityObjectToViewPos
的实现是这样的:
1 2 3
| inline float3 UnityObjectToViewPos( in float3 pos ) { return mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, float4(pos, 1.0))).xyz; }
|
为什么我们不直接使用UNITY_MATRIX_MV
去乘呢?我们看看UNITY_MATRIX_MV
的定义:
1 2
| #define UNITY_MATRIX_MV unity_MatrixMV static float4x4 unity_MatrixMV = mul(unity_MatrixV, unity_ObjectToWorld);
|
当我们直接乘的时候,是这样的:
1
| mul(mul(unity_MatrixV, unity_ObjectToWorld), float4(pos, 1.0));
|
它的计算次数是$4\times 4 \times 4 + 4 \times 4 \times 1 = 80$
而先做后两个的乘法,运算次数是$4 \times 4 \times 1 + 4 \times 4 \times 1 = 32$
省了一半之多!
但如果我们直接去乘,会出现非常严重的畸变:
可是明明庄懂那Shader Forge跑出来就很正常!为了搞清楚这个问题,我特意安装了Unity 5.6。看一下它的Shader,长这样:
1 2
| float2 sceneUVs = (i.screenPos.xy / i.screenPos.w); float2 texTrans = partZ * (sceneUVs * 2 - 1).rg * _TextureScale;
|
这里它首先从[0,pw]
映射到了[0,1]
,接下来多了一步操作,从[0,1]
映射到了[-1,1]
。原来屏幕空间实际上呈现这么一张图的样子:
现在我们变成了这样:
这样有什么好处呢?畸变的产生实际上是因为在原来比较大的$x,y$乘上深度之后就变得很大导致的,而想要杜绝这一点,我们就需要把$x,y$控制在一个比较小的范围内。进行remap操作之后,原来在屏幕就会产生的畸变程度现在在屏幕边缘产生,位于中间的深度有比较好的效果,并且越是在画面中间,畸变程度就越低。
这里我们就不用tex2Dproj
了,直接使用tex2D
进行采样:
1
| fixed4 textureColor = 1 - tex2D(_BatchTex, TRANSFORM_TEX(texTrans, _BatchTex));
|
这个问题解决了,实际上最麻烦的地方就解决了。剩下的部分就比较轻松愉快了。
首先还是先算Lambert:
1 2 3
| float3 lightDir = UnityWorldSpaceLightDir(i.worldPos); float3 worldNormal = normalize(i.worldNormal); float diffuse = dot(lightDir, worldNormal);
|
然后我们根据textureColor.r
对diffuse
进行step
操作。这样,当diffuse < 0
的时候,texColor
恒为黑色。然后我们还需要高光,这个时候可以根据某个阈值比如0.9
对diffuse
进行step操作,保留高光位点。最后,把两个step进行max,就完成了亮处高光、暗处黑色、其他地方条纹的着色。
1 2 3
| float diffuseStep = step(textureColor.r , diffuse); float whiteDiffuseStep = step(.9, diffuse); float mixStep = max(diffuseStep, whiteDiffuseStep);
|
由于此时的颜色只有两种,我们可以使用一个双色的RampTex进行采样。
1
| fixed4 color = tex2D(_RampTex, float2(mixStep, .5));
|
我们还需要一个描边效果,可以用一个经典Pass来完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| Pass { Cull Front
CGPROGRAM
#pragma vertex vert #pragma fragment frag
#include "UnityCG.cginc"
float _Outline; fixed4 _OutlineColor;
float4 vert(appdata_base i) : SV_POSITION { return UnityObjectToClipPos(float4(i.vertex.xyz+i.normal*_Outline, 1)); }
fixed4 frag() : SV_TARGET { return _OutlineColor; }
ENDCG }
|
最后我们放出完整的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| Shader "ZhuangDong/Lec2/ScreenBatch" { Properties { _BatchTex ("Texture", 2D) = "white" {} _TextureScale("Texture Scale", float) = 1000 _RampTex ("Texture", 2D) = "white" {} _Outline ("Outline", Range(0, .1)) = .1 _OutlineColor ("Outline Color", color) = (0,0,0,0) } SubShader { Pass { Cull Front
CGPROGRAM
#pragma vertex vert #pragma fragment frag
#include "UnityCG.cginc"
float _Outline; fixed4 _OutlineColor;
float4 vert(appdata_base i) : SV_POSITION { return UnityObjectToClipPos(float4(i.vertex.xyz+i.normal*_Outline, 1)); }
fixed4 frag() : SV_TARGET { return _OutlineColor; }
ENDCG
}
Pass {
CGPROGRAM #pragma vertex vert #pragma fragment frag
#include "UnityCG.cginc"
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; };
struct v2f { float4 screenPos : TEXCOORD1; float4 vertex : SV_POSITION; float3 worldPos : TEXCOORD2; float3 worldNormal : TEXCOORD3; float dep : TEXCOORD4; };
sampler2D _BatchTex; float4 _BatchTex_ST; float _TextureScale; sampler2D _RampTex; float4 _RampTex_ST;
v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.screenPos = ComputeScreenPos(o.vertex); COMPUTE_EYEDEPTH(o.screenPos.z); float dep = max(0, o.screenPos.z - _ProjectionParams.g); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal = UnityObjectToWorldNormal(v.normal); o.dep = dep; return o; }
fixed4 frag (v2f i) : SV_Target { float3 lightDir = UnityWorldSpaceLightDir(i.worldPos); float3 worldNormal = normalize(i.worldNormal); float diffuse = dot(lightDir, worldNormal); float partZ = max(0, i.screenPos.z - _ProjectionParams.g); float2 sceneUVs = (i.screenPos.xy / i.screenPos.w); float2 texTrans = partZ * (sceneUVs * 2 - 1).rg * _TextureScale; fixed4 textureColor = 1 - tex2D(_BatchTex, TRANSFORM_TEX(texTrans, _BatchTex)); float diffuseStep = step(textureColor.r , diffuse); float whiteDiffuseStep = step(.9, diffuse); float mixStep = max(diffuseStep, whiteDiffuseStep); fixed4 color = tex2D(_RampTex, float2(mixStep, .5)); return color; } ENDCG } } }
|
halftone
前两个复现出来都差强人意,这个应该还算比较正常(
和上一个shader有很多类似的地方,首先我们定义_DotSize
是点的密度。为了保证点是个圆形,比起上个shader加一步骤:
1 2 3
| float2 sceneUVs = (i.screenPos.xy / i.screenPos.w); float2 texTrans = (sceneUVs * 2 - 1).rg * _DotSize; texTrans.y = texTrans.y * _ScreenParams.y / _ScreenParams.x;
|
接下来,我们取texTrans
的小数部分。
可以看到这么一个格,这个格左下角是$(0,0)$,右上角是$(1,1)$。为了找到圆心,我们Remap到$(-.5, .5)$:
1 2
| float2 texRemap = texFrag - .5;
|
此时,texRemap
的模就是距离圆心的距离:
1 2
| float dist = length(texRemap);
|
接下来进行lambert。不同的是,我们先remap一下:
1 2 3 4
| float3 lightDir = UnityWorldSpaceLightDir(i.worldPos); float3 worldNormal = normalize(i.worldNormal); float diffuse = max(0, dot(lightDir, worldNormal)); float diffuseRemap = (1 - diffuse) * 2.5 - .5;
|
diffuse
被remap到了[-0.5,2]
的范围内。这么做是为了进行下一步:
1
| float distPow = pow(dist, diffuseRemap);
|
这么做的目的我们分类讨论:
- 比较暗的地方,diffuse小,1-diffuse大,映射到$[1,2]$的区间。此时,会让原来比较暗的点更暗,比较亮的点也比较暗。
- 比较亮的地方,diffuse大,1-diffuse小,映射到$[-.5,0]$的区间。此时,会让原来比较暗的点更亮,比较亮的点也更亮。
- 中间的地方,映射到$[0,1]$的区间,变化不是很剧烈。
这个时候做出来是这样:
为了让它更锐利,可以进行一下round操作:
1
| float roundPow = round(distPow);
|
最后再加个阴影,就完整了。
完整的Shader:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| Shader "ZhuangDong/Lec2/ScreenDot" { Properties { _DotSize ("Dot Size", float) = 10 _RampTex ("Texture", 2D) = "white" {} _Outline ("Outline", Range(.01, .05)) = .1 _OutlineColor ("Outline Color", color) = (0,0,0,0) } SubShader { Pass { Cull Front
CGPROGRAM
#pragma vertex vert #pragma fragment frag
#include "UnityCG.cginc"
float _Outline; fixed4 _OutlineColor;
float4 vert(appdata_base i) : SV_POSITION { return UnityObjectToClipPos(float4(i.vertex.xyz+i.normal*_Outline, 1)); }
fixed4 frag() : SV_TARGET { return _OutlineColor; }
ENDCG
}
Pass {
CGPROGRAM #pragma vertex vert #pragma fragment frag
#include "UnityCG.cginc" #include "AutoLight.cginc"
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; SHADOW_COORDS(0) };
struct v2f { float4 screenPos : TEXCOORD1; float4 vertex : SV_POSITION; float3 worldPos : TEXCOORD2; float3 worldNormal : TEXCOORD3; };
float _DotSize; sampler2D _RampTex; float4 _RampTex_ST;
v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.screenPos = ComputeScreenPos(o.vertex); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal = UnityObjectToWorldNormal(v.normal); TRANSFER_SHADOW(o); return o; }
fixed4 frag (v2f i) : SV_Target { float3 lightDir = UnityWorldSpaceLightDir(i.worldPos); float3 worldNormal = normalize(i.worldNormal); float diffuse = max(0, dot(lightDir, worldNormal)); float diffuseRemap = (1 - diffuse) * 2.5 - .5; float2 sceneUVs = (i.screenPos.xy / i.screenPos.w); float2 texTrans = (sceneUVs * 2 - 1).rg * _DotSize; texTrans.y = texTrans.y * _ScreenParams.y / _ScreenParams.x; float2 texFrag = frac(texTrans); float2 texRemap = texFrag - .5; float dist = length(texRemap); float distPow = pow(dist, diffuseRemap); float roundPow = round(distPow); UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); return fixed4(roundPow, roundPow, roundPow, 1.0); } ENDCG } } }
|