这里有Surface Shader的一些例子。下面的这些例子关注使用内建的光照模型;关于如何使用自定义光照模型的例子参见Surface Shader Lighting Examples。
简单
我们将会以一个非常简单的shader作为开始,并在此基础上逐渐完善。下面这个shader会把表面颜色置成“白色”。它使用内建的Lambert(漫反射)光照模型。
Shader "Example/Diffuse Simple" { SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf Lambert struct Input { float4 color : COLOR; }; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = 1; } ENDCG } Fallback "Diffuse" }
以下是使用两个 lights 作用在某个模型上的样子:
纹理
一个全白的对象太无聊了,所以让我们添加一个纹理。我们将会在shader中添加一个 Properties block,这样我门就能在材质中获得一个纹理选择器。以下粗体为其他改变:
Shader "Example/Diffuse Texture" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf Lambert struct Input { float2 uv_MainTex; }; sampler2D _MainTex; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; } ENDCG } Fallback "Diffuse" }
法线贴图
让我们添加些法线贴图:
Shader "Example/Diffuse Bump" { Properties { _MainTex ("Texture", 2D) = "white" {} _BumpMap ("Bumpmap", 2D) = "bump" {} } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf Lambert struct Input { float2 uv_MainTex; float2 uv_BumpMap; }; sampler2D _MainTex; sampler2D _BumpMap; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap)); } ENDCG } Fallback "Diffuse" }
边缘光照
现在,试着添加一些边缘光照(Rim Lighting),来使一个对象的边缘高亮。我们将会添加一些基于表面法线和视线方向夹角的散射光线。为此,我们将会使用内建的surface shader变量viewDir。
Shader "Example/Rim" { Properties { _MainTex ("Texture", 2D) = "white" {} _BumpMap ("Bumpmap", 2D) = "bump" {} _RimColor ("Rim Color", Color) = (0.26,0.19,0.16,0.0) _RimPower ("Rim Power", Range(0.5,8.0)) = 3.0 } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf Lambert struct Input { float2 uv_MainTex; float2 uv_BumpMap; float3 viewDir; }; sampler2D _MainTex; sampler2D _BumpMap; float4 _RimColor; float _RimPower; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap)); half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal)); o.Emission = _RimColor.rgb * pow (rim, _RimPower); } ENDCG } Fallback "Diffuse" }
细节纹理
为了有不同的效果,让我们添加一个与基础纹理结合的细节纹理。细节纹理在材质中使用相同的UV,但是通常使用不同的Tiling。所以我们必须使用不同的UV坐标来作为输入。
Shader "Example/Detail" { Properties { _MainTex ("Texture", 2D) = "white" {} _BumpMap ("Bumpmap", 2D) = "bump" {} _Detail ("Detail", 2D) = "gray" {} } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf Lambert struct Input { float2 uv_MainTex; float2 uv_BumpMap; float2 uv_Detail; }; sampler2D _MainTex; sampler2D _BumpMap; sampler2D _Detail; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; o.Albedo *= tex2D (_Detail, IN.uv_Detail).rgb * 2; o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap)); } ENDCG } Fallback "Diffuse" }
使用一个棋盘格子纹理并没有多大的实际意义,但能说明效果:
屏幕空间的细节纹理
屏幕空间的细节纹理是什么样的?这对士兵的头部模型来说并没有太大意义,但是能说明内建screenPos
输入是怎么用的:
Shader "Example/ScreenPos" { Properties { _MainTex ("Texture", 2D) = "white" {} _Detail ("Detail", 2D) = "gray" {} } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf Lambert struct Input { float2 uv_MainTex; float4 screenPos; }; sampler2D _MainTex; sampler2D _Detail; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; float2 screenUV = IN.screenPos.xy / IN.screenPos.w; screenUV *= float2(8,6); o.Albedo *= tex2D (_Detail, screenUV).rgb * 2; } ENDCG } Fallback "Diffuse" }
上面移除了法线贴图,仅仅是为了使代码更短。
立方体贴图反射
这里有一个使用内建输入worldRefl的shader实现立方体贴图反射。它实际上和内建的反射/漫反射shader非常像:
Shader "Example/WorldRefl" { Properties { _MainTex ("Texture", 2D) = "white" {} _Cube ("Cubemap", CUBE) = "" {} } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf Lambert struct Input { float2 uv_MainTex; float3 worldRefl; }; sampler2D _MainTex; samplerCUBE _Cube; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb * 0.5; o.Emission = texCUBE (_Cube, IN.worldRefl).rgb; } ENDCG } Fallback "Diffuse" }
一旦将反射颜色赋值到Emission中,我们就能得到一个非常闪亮的士兵:
如果你想实现由法线贴图作用的反射效果,只需稍微再加点内容:INTERNAL_DATA
需要被添加到输入结构中,而且在你写入法线输出后,WorldReflectionVector函数用来计算每个顶点反射向量。
Shader "Example/WorldRefl Normalmap" { Properties { _MainTex ("Texture", 2D) = "white" {} _BumpMap ("Bumpmap", 2D) = "bump" {} _Cube ("Cubemap", CUBE) = "" {} } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf Lambert struct Input { float2 uv_MainTex; float2 uv_BumpMap; float3 worldRefl; INTERNAL_DATA }; sampler2D _MainTex; sampler2D _BumpMap; samplerCUBE _Cube; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb * 0.5; o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap)); o.Emission = texCUBE (_Cube, WorldReflectionVector (IN, o.Normal)).rgb; } ENDCG } Fallback "Diffuse" }
以下是一个法线贴图作用的光亮的士兵:
世界空间坐标下的切片
这里的shader通过在接近水平的环上丢弃像素的方式,“切割”对象。它通过对单个世界坐标下的像素,使用Cg/HLSL函数clip()来实现。我们将会使用内建surface shader变量worldPos。
Shader "Example/Slices" { Properties { _MainTex ("Texture", 2D) = "white" {} _BumpMap ("Bumpmap", 2D) = "bump" {} } SubShader { Tags { "RenderType" = "Opaque" } Cull Off CGPROGRAM #pragma surface surf Lambert struct Input { float2 uv_MainTex; float2 uv_BumpMap; float3 worldPos; }; sampler2D _MainTex; sampler2D _BumpMap; void surf (Input IN, inout SurfaceOutput o) { clip (frac((IN.worldPos.y+IN.worldPos.z*0.1) * 5) - 0.5); o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap)); } ENDCG } Fallback "Diffuse" }
使用顶点修改器的法线挤压(Normal Extrusion)
在顶点shader中,使用用来修改输入顶点数据的“顶点修改器”函数是可能的。这能用于程序动画、法线挤压等情况。surface shader的编译指令 vertex:functionName用来实现它,其函数参数为
appdata_full
。
以下的shader用来在材质中随法线方向将顶点移动给定的量:
Shader "Example/Normal Extrusion" { Properties { _MainTex ("Texture", 2D) = "white" {} _Amount ("Extrusion Amount", Range(-1,1)) = 0.5 } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf Lambert vertex:vert struct Input { float2 uv_MainTex; }; float _Amount; void vert (inout appdata_full v) { v.vertex.xyz += v.normal * _Amount; } sampler2D _MainTex; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; } ENDCG } Fallback "Diffuse" }
随法线方向移动顶点,生成了一个胖士兵:
逐顶点计算自定义数据(Custom data computed per-vertex)
使用顶点修改器也能用来计算顶点shader中的自定义数据,然后会被逐顶点传入到表面shader函数中。需要使用同样的编译指令vertex:functionName,但该函数在这里要接受两个参数inout appdata_full
和out Input。你还可以加入任意非内建的输入参数。
注意:这种方式下的自定义成员名不能以“uv”开头,否则将会无效。
下面的例子定义了自定义成员float customColor,在顶点函数中用于计算:
Shader "Example/Custom Vertex Data" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf Lambert vertex:vert struct Input { float2 uv_MainTex; float3 customColor; }; void vert (inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input,o); o.customColor = abs(v.normal); } sampler2D _MainTex; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; o.Albedo *= IN.customColor; } ENDCG } Fallback "Diffuse" }
在这个例子中,customColor
被设置成法线的绝对值:
更实际的用途可以是用来计算非内建输入变量所提供的任意每顶点数据;或者优化shader计算。比如,用对象顶点上,而不是表面shader里逐像素,计算边缘光照。
最终颜色修改器
可以使用“最终颜色修改器”函数修改由shader计算出的最终颜色。surface shader的编译指令 finalcolor:functionName
用来实现它,其函数参数为
Input IN, SurfaceOutput o, inout fixed4 color
。
这里有一个简单地shader将染色(tint)应用到最终颜色中。这和仅仅把染色应用到表面Albedo颜色中是不同的:这个染色也会影响任何来自光照贴图、光照探测和类似外源的颜色。
Shader "Example/Tint Final Color" { Properties { _MainTex ("Texture", 2D) = "white" {} _ColorTint ("Tint", Color) = (1.0, 0.6, 0.6, 1.0) } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf Lambert finalcolor:mycolor struct Input { float2 uv_MainTex; }; fixed4 _ColorTint; void mycolor (Input IN, SurfaceOutput o, inout fixed4 color) { color *= _ColorTint; } sampler2D _MainTex; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; } ENDCG } Fallback "Diffuse" }
使用最终颜色修改器的自定义雾
最终颜色修改器(见上文)通常用于完全实现自定义雾。雾需要影响最终计算出的像素shader颜色,这正是最终颜色修改器做的。
这里的shader应用了基于离屏幕中心距离的雾染色。它联合了拥有自定义顶点数据(雾)的顶点修改器和最终颜色修改器。当雾用在正向渲染的附加通道中,需要消隐成黑色。这个例子处理这种情况,并很好地对UNITY_PASS_FORWARDADD做了检查。
Shader "Example/Fog via Final Color" { Properties { _MainTex ("Texture", 2D) = "white" {} _FogColor ("Fog Color", Color) = (0.3, 0.4, 0.7, 1.0) } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf Lambert finalcolor:mycolor vertex:myvert struct Input { float2 uv_MainTex; half fog; }; void myvert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input,data); float4 hpos = mul (UNITY_MATRIX_MVP, v.vertex); data.fog = min (1, dot (hpos.xy, hpos.xy) * 0.1); } fixed4 _FogColor; void mycolor (Input IN, SurfaceOutput o, inout fixed4 color) { fixed3 fogColor = _FogColor.rgb; #ifdef UNITY_PASS_FORWARDADD fogColor = 0; #endif color.rgb = lerp (color.rgb, fogColor, IN.fog); } sampler2D _MainTex; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; } ENDCG } Fallback "Diffuse" }
线性雾
Shader "Example/Linear Fog" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert finalcolor:mycolor vertex:myvert sampler2D _MainTex; uniform half4 unity_FogColor; uniform half4 unity_FogStart; uniform half4 unity_FogEnd; struct Input { float2 uv_MainTex; half fog; }; void myvert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input,data); float pos = length(mul (UNITY_MATRIX_MV, v.vertex).xyz); float diff = unity_FogEnd.x - unity_FogStart.x; float invDiff = 1.0f / diff; data.fog = clamp ((unity_FogEnd.x - pos) * invDiff, 0.0, 1.0); } void mycolor (Input IN, SurfaceOutput o, inout fixed4 color) { fixed3 fogColor = unity_FogColor.rgb; #ifdef UNITY_PASS_FORWARDADD fogColor = 0; #endif color.rgb = lerp (fogColor, color.rgb, IN.fog); } void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }