Shader "Unlit/reflacta" { Properties { _ReflectColor("Color a",Color) = (1,1,1,1) //反射颜色 _ReflectAmount("Amount",Range(0,1)) = 1 //反射的强度 _Cubemap("Map",Cube) = "_Skybox"{} //使用天空盒子的立方体纹理 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float3 worldNormal :TEXCOORD0; float4 worldPos : TEXCOORD1; float3 reflactDir:TEXCOORD2; float3 worldViewDir:TEXCOORD3; }; samplerCUBE _Cubemap; fixed4 _ReflectColor; float _ReflectAmount; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex); o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos); o.reflactDir = reflect(-o.worldViewDir, o.worldNormal); return o; } fixed4 frag (v2f i) : SV_Target { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 diffuse = _LightColor0.rgb * dot(worldNormal, worldLightDir) * 0.5 + 0.5; fixed3 relection = texCUBE(_Cubemap, i.reflactDir).rgb * _ReflectColor.rgb;//通过反射方向取样立方体纹理 fixed3 color = ambient + lerp(diffuse, relection, _ReflectAmount); return fixed4(color,1); } ENDCG } } }
在一个环境为天空盒子的环境中,可以通过那个天空盒子的立方体纹理来取样作为模型贴图的映射,从而达到反射环境的效果。
立方体纹理取样
效果
在reflact函数方法中,第一个参数是入射光的方向(由远方指向顶点),第二个参数是顶点的法线。在这里,使用使用视野方向取反来替代入射光方向来求反射反向来取样立方体纹理。可以想象一下,你前面有一块镜子,你侧着看镜子,而你看的直线就是入射光,被镜子反射后的光线就是你看到的东西了。
Shader "Unlit/ForwardRendering" { Properties { } SubShader { Tags{ "RenderType" = "Opaque" } LOD 100 //第一个重要的平行光,计算环境光和自发光 Pass { Tags{ "LightMode" = "ForwardBase" } CGPROGRAM #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" struct appdata { float4 vertex : POSITION; float3 normal:NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float3 worldNormal : COLOR0; SHADOW_COORDS(2) //定义一个对阴影纹理采样的坐标 }; v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.worldNormal = mul(unity_ObjectToWorld, v.normal); TRANSFER_SHADOW(o); //将采样坐标转换到屏幕空间 return o; } fixed4 frag(v2f i) : SV_Target { fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; float3 worldLightDir = normalize(_WorldSpaceLightPos0); fixed3 diffuse = _LightColor0.rgb * max(0, dot(i.worldNormal, worldLightDir)); fixed shadow = SHADOW_ATTENUATION(i); //采样 return fixed4(ambient + diffuse * shadow,1); } ENDCG } //计算其他逐像素光照 有阴影的设置 Pass { Tags{ "LightMode" = "ForwardAdd" } Blend One One //混合 CGPROGRAM #pragma multi_compile_fwdadd_fullshadows //编译为接受阴影 #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" //unity_WorldToLight struct appdata { float4 vertex : POSITION; float3 normal:NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float3 worldNormal : COLOR0; float4 worldPos :COLOR1; SHADOW_COORDS(2) //声明阴影纹理的取样坐标 }; v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.worldNormal = mul(unity_ObjectToWorld, v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex); TRANSFER_SHADOW(o);//计算阴影纹理的取样坐标,屏幕空间 return o; } fixed4 frag(v2f i) : SV_Target { //光照方向 UnityWorldSpaceLightDir(worldPos) #ifdef USING_DIRECTIONAL_LIGHT fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); #else fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz); #endif UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos.xyz);//计算阴影和光照衰减 联合在atten中 fixed3 diffuse = _LightColor0.rgb * max(0, dot(i.worldNormal, worldLightDir)) * atten; return fixed4(diffuse,1); } ENDCG } } Fallback "Diffuse" }
使用宏UNITY_LIGHT_ATTENUATION来计算阴影和光照衰减的联合,三个参数,一目了然的。通过这个宏可以统一在Base Pass和Additional Pass无差别的计算阴影和光照衰减,达到代码的一致。
Shader "Unlit/ForwardRendering" { Properties { } SubShader { Tags{ "RenderType" = "Opaque" } LOD 100 Pass { Tags{ "LightMode" = "ForwardBase" } CGPROGRAM #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" struct appdata { float4 vertex : POSITION; float3 normal:NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float3 worldNormal : COLOR0; SHADOW_COORDS(2) //定义一个对阴影纹理采样的坐标 }; v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.worldNormal = mul(unity_ObjectToWorld, v.normal); TRANSFER_SHADOW(o); //将采样坐标转换到屏幕空间 return o; } fixed4 frag(v2f i) : SV_Target { fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; float3 worldLightDir = normalize(_WorldSpaceLightPos0); fixed3 diffuse = _LightColor0.rgb * max(0, dot(i.worldNormal, worldLightDir)); fixed shadow = SHADOW_ATTENUATION(i); //采样 return fixed4(ambient + diffuse * shadow,1); } ENDCG } } Fallback "Diffuse" //这里面有个Fallback为VertexLit定义了LightMode=ShadowCaster的Pass,所以会参与阴影投射 }
接受阴影的三剑客,SHADOW_COORDS、TRANSFER_SHADOW、SHADOW_ATTENUATION,注意这些宏使用了上下文的变量,所以这些变量名需要对应上,appdata的v的vertex,v2f的i的vertex。
Unity中使用一个LightMode设置为ShadowCaster的Pass来收集计算出阴影映射纹理(深度纹理),如果你的Shader中没有定义这样的Pass,Unity就是在Fallback中去找,知道找到或则没有Fallback为止,没有找到的话就不会参与阴影的投射但可以接受阴影。
屏幕空间的阴影映射技术,Unity5中使用了这种技术,需要显卡支持MRT。首先通过LightMode为ShadowCaster的Pass来得到可投射阴影的光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的但是却处于该光源的阴影中,这样的话,阴影图就包含了屏幕空间中所有有阴影的区域。由于阴影图是屏幕空间下的,我们将表面坐标从模型空间变换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。
Unity使用一张纹理作为查找表在片元着色器中计算逐像素光照的衰减,可以减轻复杂度。不过需要预处理得到采样纹理,纹理的大小会影响衰减的精度。
预处理出来的光照衰减纹理为_lightTexture0,如果该光源使用了cookie,那么为_LightTextureB0。使用点在光源空间的位置的点积来对纹理取样(不使用长度还要开方)。
float3 lightCoord = mul(unity_WorldToLight , float4(i.worldPos.xyz, 1)).xyz; fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord)).UNITY_ATTEN_CHANNEL;
UUNITY_ATTEN_CHANNEL是一个宏用来获取光照衰减所在的分量的。
Shader "Unlit/ForwardRendering" { Properties { } SubShader { Tags { "RenderType"="Opaque" } LOD 100 //第一个重要的平行光,计算环境光和自发光 Pass { Tags{"LightMode" = "ForwardBase"} CGPROGRAM #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" struct appdata { float4 vertex : POSITION; float3 normal:NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float3 worldNormal : COLOR0; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.worldNormal = mul(unity_ObjectToWorld, v.normal); return o; } fixed4 frag (v2f i) : SV_Target { fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;//环境光,Base这里计算一次 float3 worldLightDir = normalize(_WorldSpaceLightPos0); fixed3 diffuse = _LightColor0.rgb * max(0, dot(i.worldNormal, worldLightDir)); return fixed4(ambient + diffuse,1); } ENDCG } //计算其他逐像素光照 Pass { Tags{ "LightMode" = "ForwardAdd" } Blend One One //混合 CGPROGRAM #pragma multi_compile_fwdadd #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" //unity_WorldToLight struct appdata { float4 vertex : POSITION; float3 normal:NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float3 worldNormal : COLOR0; float4 worldPos :COLOR1; }; v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.worldNormal = mul(unity_ObjectToWorld, v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } fixed4 frag(v2f i) : SV_Target { //光照方向 #ifdef USING_DIRECTIONAL_LIGHT fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); #else fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz); #endif //计算光照衰减 #ifdef USING_DIRECTIONAL_LIGHT fixed atten = 1.0; #else float3 lightCoord = mul(unity_WorldToLight , float4(i.worldPos.xyz, 1)).xyz; fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL; #endif fixed3 diffuse = _LightColor0.rgb * max(0, dot(i.worldNormal, worldLightDir)); return fixed4(diffuse,1)* atten ; } ENDCG } } }
Pass { Tags { "LightMode" = "ForwardBase" } }
当场景有很多实时光照,前向渲染的性能就会极速下降。所以更古老的延迟渲染就有用了。延迟渲染除了颜色缓冲和深度缓冲外,还会利用额外的缓冲区叫做G缓冲。G缓冲存储了我们关心的表面(离相机近的表面)的其他信息,表面的法线、位置、用于光照计算的材质属性等。延迟渲染主要有两个Pass,第一个Pass不进行任何光照计算,仅仅计算哪些片元是可见的,然后将信息存储到G缓冲区中。然后第二个Pass,就会利用G缓冲中的片元信息,进行真正的光照计算。注意,使用延迟渲染不支持真正的抗锯齿、不能出来半透明物体。
前向渲染是一种传统的常用的渲染路径,在Unity中通过Quality Setting设置逐像素关照的个数,然后关照们根据光照的重要和距离排序决定是否是逐像素,直射光一定是逐像素的。前向渲染有两种Pass,一种是Base Pass,用来计算一个逐像素的平行光和所有逐顶点和SH光源,这个Pass只执行一次,所以环境光和自发光也是这里计算,这些是计算一次就行了。还有一种是Additional Pass,用来计算其他影响该物体的逐像素光源,每个光源执行一次Pass,这个Pass会设置混合模式,然后计算到该光源影响的颜色和帧缓存混合起来。默认的AP是没有阴影效果的,但是可以使用#pragma multi_compile_fwdadd_fullshadows来替换掉#pragma multi_compile_fwdadd编译指令。
要实现双面物体其实就是关闭剔除,物体相对摄像机来说都有正面和反面的说法,我们可以关闭剔除,从而达到正反面都渲染的目的。
Cull Front | Back | Off
透明测试的只需要在Pass通道中加上 Cull Off 就可以实现透明的双面渲染,就是透过自己看到自己后面面的背面。
混合模式的透明需要加多一个Pass通道,两个通道的内容是一样的,但是一个 Cull Front (先渲染背面),一个 Cull Back(再渲染正面)。使用双通道会消耗性能,但是不用的话,混合中没有深度写入,就会有可能的背面渲染在正面前面,所以使用两个通道,先背面再正面。
注意具有深度写入的透明物体,在第一个Pass的时候,深度值就已经把背面的片元给排除掉了。