本课程全是用Built-in渲染管线
最基本的Shader代码
默认的Unlit 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
| Shader "CS02/MiniShader" {
Properties { _MainTex ("Texture", 2D) = "" {} _Float("Float", Float) = 0.0 _Slider("Slider", Range(0.0,1.0)) = 0.07 _Vector("Vector", Vector) = (.34, .85, .92, 1) }
SubShader {
Tags { "RenderType"="Opaque" }
Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc"
struct appdata { float4 vertex : POSITION; half2 texcoord0 : TEXCOORD0; half2 texcoord1 : TEXCOORD1; half2 texcoord2 : TEXCOORD2; half2 texcoord4 : TEXCOORD3;
half4 color : COLOR; half3 normal : NORMAL; half4 tangent : TANGENT; };
struct v2f //自定义数据结构体,顶点着色器输出的数据,也是片元着色器输入数据 { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 normal : TEXCOORD1; };
sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord0 * _MainTex_ST.xy + _MainTex_ST.zw; o.normal = v.normal; return o; } half4 frag (v2f i) : SV_Target { half4 col = tex2D(_MainTex, i.uv); return col; } ENDCG } } }
|
Properties内声明的属性值一部分为Unity序列化用,我们还需要在SubShader——Pass块里面再次声明这些属性。
1 2 3 4
| 精度标准: float(32bit) :坐标点 half(16bit) :UV、大部分向量 fixed(8bit) :颜色
|
注意在v2f
结构体中,精度都为float
在Shader代码中,如果顶点着色器向片元传递的是向量,float(x,x,x,0.0)
最后一位给0,如果顶点着色器向片元传递的是点,float(x,x,x,1.0)
最后一位给1。
Shader代码中,两个多维对象是可以进行乘算的,比如Vector2 * Vector2,多维对象的每一维单独相乘,如果需要点乘或叉乘就使用Dot和Cross
贴图映射
背面剔除
背面剔除可以使用相关的属性声明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Properties { [Enum(UnityEngine.Rendering.CullMode)]_CullMode("CullMode", float) = 2 } SubShader { Pass { Cull [_CullMode] CGPROGRAM ENDCG } }
|
uv映射到平面
一般的模型,uv信息存储在TEXCOORD0
里,我们把这个信息通过vertex传递给fragment进行映射。但是我们可以不要这个uv信息,我们直接使用顶点坐标vertex : POSITION
进行映射,这样就可以让一张贴图直接映射到一个模型的xy平面(或者yz平面、xz平面)
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
| struct appdata { float4 vertex : POSITION; half2 uv0 : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float2 pos_uv : TEXCOORD1; }; sampler2D _MainTex; float4 _MainTex_ST;
v2f vert(appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.uv0 * _MainTex_ST.xy + _MainTex_ST.zw; o.pos_uv = v.vertex.xy * _MainTex_ST.xy + _MainTex_ST.zw; }
half4 frag(v2f i) { half4 col = tex2D(_MainTex, i.pos_uv); return col; }
|


uv不合理导致贴图失真
在模型面数有限的情况下,贴图会因为uv排布不合理导致失真

上面的图因为uv不合理导致直线不直
美术需要在模型制作阶段就进行uv检查,包括贴图精度和缩放一致性。
如果遇到上述失真的情况,增加模型的细分可以解决。但是不推荐。也可以在片元着色器中重新计算uv坐标,但是会引发其他问题。
AlphaTest
在片元Shader执行完毕后,获得的片元数据进入输出合并阶段:
Alpha Test —— Stencil Test —— Depth Test —— Blending —— FrameBuffer(帧缓冲区)
利用Alpha Test,某些透明像素就会被扔掉,可以实现某些特效(边缘比较锐利的特效,因为Alpha Test是直接扔掉像素)
在实际Shader编写中,主要使用clip
函数来实现Alpha剔除的效果。需要记住这里舍弃的是整个片元,所以我们直接返回颜色。
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
| Shader "KerryTA/03ClipShader" { Properties { _MainColor("MainColor",Color) = (1,1,1,1) _MainTex("Texture",2D) = "white"{} _NoiseTex("Noise",2D) = "white"{} _Cutout("Cutout",Range(-0.1,1.1)) = 0.0 _Speed("Speed",Vector) = (1,1,0,0) [Enum(UnityEngine.Rendering.CullMode)]_CullMode("CullMode", float) = 2 }
SubShader { Tags { "RenderType"="Opaque"} Pass { Cull [_CullMode] CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc"
struct appdata { float4 vertex : POSITION; half2 texcoord0 : TEXCOORD0; };
struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };
float4 _MainColor; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _NoiseTex; float4 _NoiseTex_ST; float _Cutout; float4 _Speed;
v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord0 * _MainTex_ST.xy + _MainTex_ST.zw; return o; }
half4 frag (v2f i) : SV_Target { half gradient = tex2D(_MainTex,i.uv + _Time.y * _Speed.xy).r; half noise = tex2D(_NoiseTex,i.uv + _Time.y * _Speed.zw).r; clip(gradient - noise - _Cutout); return _MainColor; }
ENDCG } } }
|
时间属性_Time
是个四维变量,这里取y。注意使用时间来偏移uv时,在片元代码中修改uv。
Name |
Type |
Value |
_Time |
float4 |
Time since level load (t/20, t, t*2, t*3), use to animate things inside the shaders. |
_SinTime |
float4 |
Sine of time: (t/8, t/4, t/2, t). |
_CosTime |
float4 |
Cosine of time: (t/8, t/4, t/2, t). |
unity_DeltaTime |
float4 |
Delta time: (dt, 1/dt, smoothDt, 1/smoothDt). |
Blend
在Pass语义块中CGPROGRAM上面添加Blend
和ZWrite Off
,透明物体前后遮挡时必须关闭ZWrite,否则后面的片元都会被舍弃。
在SubShader的Tags中将Queue指定为Transparent
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
| Shader "KerryTA/04BlendingShader" { Properties { _MainTex("Texture",2D) = "white"{} _MainColor("MainColor",Color) = (1,1,1,1) _Emiss("Emission",Float) = 1.0 [Enum(UnityEngine.Rendering.CullMode)]_CullMode("CullMode", float) = 2 }
SubShader { Tags { "Queue" = "Transparent"} Pass { ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Cull [_CullMode] CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc"
struct appdata { float4 vertex : POSITION; half2 texcoord0 : TEXCOORD0; };
struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };
sampler2D _MainTex; float4 _MainTex_ST; float4 _MainColor; float _Emiss;
v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord0 * _MainTex_ST.xy + _MainTex_ST.zw; return o; }
half4 frag (v2f i) : SV_Target { half3 col = _MainColor.xyz * _Emiss; half alpha = saturate(tex2D(_MainTex,i.uv).r * _MainColor.w * _Emiss); return half4(col,alpha); }
ENDCG } } }
|
边缘光
边缘光的原理就是在片元着色器中求当前片元到摄像机的向量和当前片元世界空间下的法向量的点乘(Dot),根据结果舍弃掉一些夹角比较小的片元。
注意顶点函数中求世界空间下的normal时,使用的是normalize(mul(float4(v.normal,0.0), _World2Object).xyz)
,这是因为法线不能因为模型单个轴的缩放导致偏移,法线有自己的变换矩阵,经过公式推导后我们求世界空间下的法线时就需要法向量右乘世界到模型的矩阵。
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 "KerryTA/05RimShader" { Properties { _MainColor("MainColor",Color) = (1,1,1,1) _Emiss("Emission",Float) = 1.0 _RimPower("RimPower",Float) = 1.0 }
SubShader { Tags { "Queue" = "Transparent"} Pass { ZWrite Off Blend SrcAlpha One CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc"
struct appdata { float4 vertex : POSITION; half2 texcoord0 : TEXCOORD0; float3 normal : NORMAL; };
struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 normal_world : TEXCOORD1; float3 view_world : TEXCOORD2; };
float4 _MainColor; float _Emiss; float _RimPower;
v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.normal_world = normalize(mul(float4(v.normal,0.0), unity_WorldToObject).xyz); float3 pos_world = mul(unity_ObjectToWorld,v.vertex).xyz; o.view_world = normalize(_WorldSpaceCameraPos.xyz - pos_world); return o; }
half4 frag (v2f i) : SV_Target { float3 normal_world = normalize(i.normal_world); float3 view_world = normalize(i.view_world); float NdotV = saturate(dot(normal_world,view_world)); float fresnel = pow(1.0 - NdotV, _RimPower); float alpha = saturate(fresnel * _Emiss); float3 col = _MainColor.xyz * _Emiss; return float4(col,alpha); }
ENDCG } } }
|
深度预写入
利用上面的Shader制作的边缘光还不够完美,即使我们设置了ZWrite On
,在实际界面中我们会发现模型被遮挡的背面依然会渲染。这是因为在同一个模型下,使用ZWrite来去掉背面有一定问题,就是每一个顶点的深度一边计算一边写入,如果后面的顶点比前面的顶点预先渲染,那么前面的顶点就不能遮挡后面的。
想要完美解决这个问题,就需要再加一个Pass,放在边缘光Pass的前面,在这个Pass中,我们预先将所有顶点的深度计算出来。这样就能解决上述问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| Pass { Cull Off ZWrite On ColorMask 0 CGPROGRAM float4 _Color; #pragma vertex vert #pragma fragment frag
float4 vert(float4 vertexPos : POSITION) : SV_POSITION { return UnityObjectToClipPos(vertexPos); } float4 frag(void) : COLOR { return _Color; } ENDCG }
|