Unity-SmipleWater

这次我们将运用之前所学的内容进行简单的水体编辑,先上效果图。

完成后的效果图

这次的水体其实非常的简单,主要是通过UV运动与顶点运动所实现的,为了节省美术资源,我们就用了两张图像资源,一张包含渐变、泡沫的Foam图,一张法线图。

Foam
Foam
Normal
Normal

我们所使用的资源就是这两个。首先我们先完成海水的渐变效果,在这里我们需要用到Foam的R通道,如果要完成更加自由的组合我们可以用photoshop来进行编辑或者设置两个颜色与一张灰度图来进行比较,这两种方法都会在日常的工作中运用得到。但这里我们只用Foam图的R通道进行实现渐变。

fixed degree=tex2D(_Foma,i.uv).r;
fixed3 albedo=lerp(_ShalowColor,_DeepColor,degree);

如上所写,我们将_Foam进行采样然后将他的R通道的值赋予degree,之后将albedo也就是反射率进行线性插值,便可以得到如下效果

简单的渐变与法线

与法线贴图配合就能实现上图的效果。这部分的代码比较重要,建议写的比较有条理些,毕竟框架蛮重要的。

Shader "Custom/SeaWater"
{
    Properties
    {
        [Space(20)]
        [Header(Color)]
        _ShalowColor("Shalow Color",Color)=(1,1,1,1)
        _DeepColor("Deep Color",Color)=(1,1,1,1)
        
        
        [Space(20)]
        [Header(Texture)]
        _Foam("Foam",2D)="white"{}
        _WaterNormal("Water Normal",2D)="Bump"{}
        _NormalScale("Normal Scale",Float)=1.0

        [Space(20)]
        [Header(Specular)]
        _SpecularColor("Specular Color",Color)=(1,1,1,1)
        _SpecularScale("Specular Scale",Range(8,256))=20
    }
    SubShader
    {
        Pass{
            Tags { "RenderType"="Opaque" }
            CGPROGRAM
            #pragma vertex vert 
            #pragma fragment frag 
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"
            #include "Lighting.cginc"

            fixed4 _ShalowColor;
            fixed4 _DeepColor;
            fixed4 _SpecularColor;
            float _SpecularScale;

            sampler2D _Foam;
            float4 _Foam_ST;
            sampler2D _WaterNormal;
            float4 _WaterNormal_ST;

            float _NormalScale;

            struct a2v{
                float4 vertex:POSITION;
                float4 texcoord:TEXCOORD0;
                float3 normal:NORMAL;
                float4 tangent:TANGENT;
            };

            struct v2f{
                float4 pos:SV_POSITION;
                float4 uv:TEXCOORD0;
                float3 lightDir:TEXCOORD1;
                float3 viewDir:TEXCOORD2;
            };

            v2f vert(a2v v){
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);

                o.uv.xy = v.texcoord.xy * _Foam_ST.xy + _Foam_ST.zw;
			    o.uv.zw = v.texcoord.xy * _WaterNormal_ST.xy + _WaterNormal_ST.zw;

                TANGENT_SPACE_ROTATION;

                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
                o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;

                return o;
            }

            fixed4 frag(v2f i):SV_Target{
                float3 tangentLightDir=normalize(i.lightDir);
                float3 tangentViewDir=normalize(i.viewDir);
                fixed4 packedNormal = tex2D(_WaterNormal, i.uv.zw);
			    fixed3 tangentNormal=UnpackNormal(packedNormal);
                tangentNormal.xy *=_NormalScale;
                tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

                fixed degree=tex2D(_Foam,i.uv.xy).r;
                fixed3 albedo=lerp(_ShalowColor,_DeepColor,degree);
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));

                fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
                fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(tangentNormal, halfDir)), _SpecularScale);
				
			    return fixed4(ambient+diffuse + specular, 1.0);
            
            }
            ENDCG
        }
    }

    FallBack "Diffuse"
}

我们先将框架搭完了之后就是传统的搭积木写代码而已。法线贴图的主要思路可以参考网上的代码,这部分资料有很多,我比较推荐冯乐乐的《Shader 入门精要》,里面阐述的比较详细。

接着我们进行最关键的一部分代码的书写,水面波纹的运动。毕竟我们的运动主要由法线动画所体现,核心代码如下

half2 panner1 = ( _Time.y * _WaveParams.xy + i.uv);
half2 panner2 = ( _Time.y * _WaveParams.zw + i.uv);
half3 worldNormal = BlendNormals(UnpackNormal(tex2D( _WaterNormal, panner1)) , UnpackNormal(tex2D(_WaterNormal, panner2)));
worldNormal = lerp(half3(0, 0, 1), worldNormal, _NormalScale);

我们在这里新定义了一个参数_WaveParams(x,y,z,w),这个参数我们UE4中经常用到是4位的向量,我们在这里可以定义一下,海浪参数(x:海浪范围,y:海浪偏移,z:海浪扰动,w:浪花泡沫扰动),这里我要啰嗦一句,在进行法线贴图的时候,法线其实是储存在切线空间下的,我当要使用法线在世界中的坐标的时候需要进行一部分转换。具体可以看这篇文章。关于为何要将法线存储在切线文章,可以参考法线贴图这篇文章。具体的关键代码如下:

v2f vert (appdata_full v)
{
    ...
    o.worldPos = mul(unity_ObjectToWorld, v.vertex);
    fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
    fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
    fixed tangentSign = v.tangent.w * unity_WorldTransformParams.w;
    fixed3 worldBinormal = cross(worldNormal, worldTangent) * tangentSign;
    o.TW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, o.worldPos.x);
    o.TW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, o.worldPos.y);
    o.TW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, o.worldPos.z);
    ...
    return o;
}
fixed4 frag (v2f i) : SV_Target
{
    ...
    worldNormal = normalize(fixed3(dot(i.TW0.xyz, worldNormal), dot(i.TW1.xyz, worldNormal), dot(i.TW2.xyz, worldNormal)));
    ...
}
uv动画
uv动画

制作高光的时候我这里选择的Blinn-Phong高光模型,关于Blinn-Phong模型我们这里不再赘述,网上有很多资料,本博客也有。

fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
                fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(tangentNormal, halfDir)), _SpecularScale);

我们在打框架的时候已经将Blinn-Phong模型加了进去,效果图就不放了,接下来我们要加入Fresnel效果,关于Fresnel有一篇很著名的博文EveryThingHasFresnel大家如果有空可以去看一看,Fresnel有一个比较类似的公式

Fresnel Schlick公式

其中F0意味着Fresnel系数,由我们自己定义。其他的都是由我们在shader中获取。还有一个比较广泛的公式是fresnel Empricial如下

Fresnel Empricial公式

其中Blas、Scale、Power是可控项,这个公式具有更高的可控性,大家可以玩玩,挺有意思的。

但,我们这里一个都不用,主要是具体介绍下Fresnel公式,我们这里用一个trick来实现fresnel,就是用自发光来代替不同角度下的颜色。

fixed3 rim = pow(1-saturate(dot(tangentNormal,tangentViewDir)),_RimPower)*_LightColor0.rgb * _RimIntensity;
对比图

接着是我们另外一个难点,就是海浪的绘制,海浪的绘制有两个方法,一是用通道图画出边缘。优点是可控性强,性能消耗低。缺点是边缘质量很依赖贴图大小和精度,一旦场景改变需要重制贴图;二是使用深度图直接检测出交接边缘。优点是精度较高,无需美术反复修改贴图。缺点是性能消耗略大(有些低端移动设备不支持渲染深度图或者默认不开启渲染深度图)。这里采用深度图做法。深度图我们需要在Camera中开启深度渲染,这一步只需要再脚本中写入

Camera.main.depthTextureMode = DepthTextureMode.Depth;
开启深度渲染的摄像机组件

在Shader中我们首先要先声明深度纹理

uniform sampler2D _CamerDepthTexture;

获取屏幕位置

o.screenPos = ComputeScreenPos(o.vertex);

计算边缘区域

half4 screenPos = float4( i.screenPos.xyz , i.screenPos.w);
half eyeDepth = LinearEyeDepth(UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture,UNITY_PROJ_COORD( screenPos ))));
half eyeDepthSubScreenPos = abs( eyeDepth - screenPos.w );
half depthMask = 1-eyeDepthSubScreenPos + _FoamDepth;

计算边缘区域的逻辑其实非常简单,用摄像机距离地形的距离减去摄像机距离平面的距离,然后取得绝对值。该绝对值约接近为0则越可能是其边缘,添加FoamDepth是为了控制边缘区域的范围大小

他的绿色部分就是我们所需要的值,该值的求解就是黄色的线减去红色的线,可以非常容易理解通过深度图是如何进行取得接触边缘的的。

在这里需要强调一点的是当开启深度纹理的时候需要在shader的FallBack设置正确的”Diffuse”能够体现深度的设置。这点很重要,不然Shader不会报错,但依旧的不到正确的结果。

有了边缘区域接下来就是要让边缘显示为泡沫形状。最简单的做法就是使用泡沫通道乘以遮罩然后对水颜色和泡沫颜色进行插值。参考代码如下:

half3 foam = tex2D(_Foam,i.uv);
float temp_output = ( saturate( (foam.g * depthMask - _FoamFactor) );
diffuse = lerp( diffuse , _FoamColor, temp_output);

为了使边缘不规则,我们可以增加噪声图或者直接新型采样,depthMask *=water.g 这部分的water可以是

half3 water = tex2D(_Foam,i.uv.xy/_Foam_ST.xy);

精简下代码就是

depthMask *=tex2D(_Foam,i.uv.xy/_Foam_ST.xy).g;

此时我们就得到了初步的岸边的效果。

我们同样对水面进行UV的动画处理

half3 foam1 = tex2D(_Foam,i.uv + worldNormal.xy*_FoamOffset.w);
half3 foam2 = tex2D(_Foam, _Time.y * _FoamOffset.xy + i.uv + worldNormal.xy*_FoamOffset.w);

现在细节只是一层不动的颜色贴图,在这基础上添加扰动,让它更像海水的状态:

alf3 foam1 = tex2D(_Foam,i.uv + worldNormal.xy*_FoamOffset.w);
half3 foam2 = tex2D(_Foam, _Time.y * _FoamOffset.xy + i.uv + worldNormal.xy*_FoamOffset.w);

Foamoffset.w是扰动因子然后修改diffuse的颜色与细节颜色混合。

half4 detail = tex2D(_Foam,i.uv/_Foam_ST.xy).b * _DetailColor;
diffuse.rgb = fixed3(diffuse.rgb * (NdotV + detail.rgb) * 0.5);

现在细节只是一层不动的颜色贴图,在这基础上添加扰动,让它更像海水的状态:

half2 detailpanner = (i.uv/_Foam_ST.xy + worldNormal.xy*_WaterWave);
half4 detail = tex2D(_Foam,i.uv/_Foam_ST.xy).b * _DetailColor;

接下来我们需要进行顶点动画的制作,我们首先将原来的Plane替换成几万顶点的Mesh。顶点动画的代码之前的文章有过叙述。

float time = _Time.y * _Speed;
float waveValue = sin(time + v.vertex.x *_Frequency)* _Amplitude;
v.vertex.xyz = float3(v.vertex.x, v.vertex.y + waveValue, v.vertex.z);

speed是移动速度,Frequency是频率,Amplitude是幅度。

最后我们将标签改为Transparent进行透明度渲染。

half alpha = saturate(eyeDepthSubScreenPos-_AlphaWidth);
fixed4 col = fixed4( diffuse + specular + rim ,alpha);

整体代码如下:

Shader "Custom/SeaWater"
{
    Properties
    {
        [Space(20)]
        [Header(Color)]
        _ShalowColor("Shalow Color",Color)=(1,1,1,1)
        _DeepColor("Deep Color",Color)=(1,1,1,1)
        _FoamColor("Foam Color",Color)=(1,1,1,1)
        _DetailColor("Detail Color",Color)=(1,1,1,1)
        
        
        [Space(20)]
        [Header(Texture)]
        _Foam("Foam",2D)="white"{}
        _WaterNormal("Water Normal",2D)="Bump"{}
        _NormalScale("Normal Scale",Float)=1.0

        [Space(20)]
        [Header(Specular)]
        _SpecularColor("Specular Color",Color)=(1,1,1,1)
        _SpecularScale("Specular Scale",Float)=1.0
        _Gloss("Gloss",Float)=20

        [Space(20)]
        [Header(Animation)]
        _WaveParams("Wave Params",Vector)=(0,0,0,0)
        _FoamOffset("Foam Offset",Vector)=(0,0,0,0)
        _WaterWave("Water Wave",Float)=1.0
        _WaveXSpeed("Wave Horizontal Speed",Range(-0.1,0.1))=0.01
		_WaveYSpeed("Wave Vertical Speed",Range(-0.1,0.1))=0.01
		_Amplitude("Amplitude",Float)=1.0
		_Frequency("Frequency",Float)=1.0
		_Speed("Speed",Float)=1.0
        
        [Space(20)]
        [Header(Emission)]
        _RimPower("Rim Power",Float)=1.0
        _RimIntensity("RimIntensity",Float)=1.0

        [Space(20)]
        [Header(Foam)]
        _FoamDepth("Foam Depth",Float)=1.0
        _FoamFactor("Foam Factor",Float)=1.0

        [Space(20)]
        [Header(Alpha)]
        _AlphaWidth("AlphaWidth",Float)=1.0


    }
    SubShader
    {
        Pass{
            Tags { "LightMode"="ForwardBase" "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
            ZWrite On 
			Blend SrcAlpha OneMinusSrcAlpha
            
            CGPROGRAM
            #pragma vertex vert 
            #pragma fragment frag 
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"
            #include "Lighting.cginc"

            fixed4 _ShalowColor;
            fixed4 _DeepColor;
            fixed4 _SpecularColor;
            fixed4 _FoamColor;
            fixed4 _DetailColor;
            float _SpecularScale;
            float _Gloss;

            sampler2D _Foam;
            float4 _Foam_ST;
            sampler2D _WaterNormal;
            float4 _WaterNormal_ST;

            float _NormalScale;

            float4 _WaveParams;
            fixed4 _FoamOffset;
            float _WaterWave;
            fixed _WaveXSpeed;
			fixed _WaveYSpeed;
            float _Amplitude;
			float _Frequency;
			float _Speed;
            float _AlphaWidth;


            float _RimPower;
            float _RimIntensity;

            float _FoamDepth;
            float _FoamFactor;
            uniform sampler2D _CameraDepthTexture;



            struct a2v{
                float4 vertex:POSITION;
                float4 texcoord:TEXCOORD0;
                float3 normal:NORMAL;
                float4 tangent:TANGENT;
            };

            struct v2f{
                float4 pos:SV_POSITION;
                float4 uv:TEXCOORD0;
                float3 lightDir:TEXCOORD1;
                float3 viewDir:TEXCOORD2;
                float4 screenPos:TEXCOORD3;
            };

            v2f vert(a2v v){
                v2f o;
                
                float time=_Time.y*_Speed;
				float waveValue=sin(time+v.vertex.x*_Frequency)*_Amplitude;
				v.vertex.xyz=float3(v.vertex.x,v.vertex.y+waveValue,v.vertex.z);
                
                o.pos=UnityObjectToClipPos(v.vertex);

                o.uv.xy = v.texcoord.xy * _Foam_ST.xy + _Foam_ST.zw;
			    o.uv.zw = v.texcoord.xy * _WaterNormal_ST.xy + _WaterNormal_ST.zw;

                TANGENT_SPACE_ROTATION;

                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
                o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
                
                o.screenPos = ComputeScreenPos(o.pos);

                return o;
            }

            fixed4 frag(v2f i):SV_Target{
                float3 tangentLightDir=normalize(i.lightDir);
                float3 tangentViewDir=normalize(i.viewDir);

                half2 panner1 = ( _Time.y * _WaveParams.xy + i.uv.zw);
                half2 panner2 = ( _Time.y * _WaveParams.zw + i.uv.zw);
                half3 tangentNormal = BlendNormals(UnpackNormal(tex2D( _WaterNormal, panner1)) , UnpackNormal(tex2D(_WaterNormal, panner2)));
                tangentNormal = lerp(half3(0, 0, 1), tangentNormal, _NormalScale);
                tangentNormal.xy *=_NormalScale;
                tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
                
                half4 screenPos = float4( i.screenPos.xyz , i.screenPos.w);
                half eyeDepth = LinearEyeDepth(UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture,UNITY_PROJ_COORD( screenPos ))));
                half eyeDepthSubScreenPos = abs( eyeDepth - screenPos.w );
                half depthMask = 1-eyeDepthSubScreenPos + _FoamDepth;
                depthMask *=tex2D(_Foam,i.uv.xy/_Foam_ST.xy).g;

                half3 foam1 = tex2D(_Foam,i.uv.xy+tangentNormal.xy*_FoamOffset.w);
				half3 foam2 = tex2D(_Foam, _Time.y * _FoamOffset.xy + i.uv.xy+tangentNormal.xy*_FoamOffset.w);
                float temp_output = ( saturate( (foam1.g + foam2.g ) * depthMask - _FoamFactor));
                

                fixed degree=tex2D(_Foam,i.uv.xy).r;
                fixed3 albedo=lerp(_ShalowColor,_DeepColor,degree);

                half2 detailpanner=i.uv.xy/_Foam_ST.xy+tangentNormal.xy*_WaterWave;
				half4 detail=tex2D(_Foam,i.uv.xy/detailpanner).b*_DetailColor;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 rim = pow(1-saturate(dot(tangentNormal,tangentViewDir)),_RimPower)*_LightColor0.rgb * _RimIntensity;

                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
                diffuse = lerp( diffuse , _FoamColor, temp_output);
                diffuse.rgb=fixed3(diffuse.rgb*(dot(tangentNormal,tangentViewDir)+detail.rgb)*0.5);
                //half alpha = saturate(eyeDepthSubScreenPos-_AlphaWidth);
                
                
                fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
                fixed specularMask = tex2D(_WaterNormal, i.uv.zw).r * _SpecularScale;
                fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss)*specularMask;
				
			    return fixed4(ambient+diffuse+specular+rim, _AlphaWidth);
            
            }
            ENDCG
        }
    }

    FallBack "Diffuse/Specular/Transparent"
}

整体的效果到这就结束了,Shader的思路就是搭积木,选好模型,然后不断添加细节。在这基础之上可以添加交互功能,具体的方法我是参考这篇Blgo。可以得到基础的效果。另外关于海面波光的表现形式有多种做法,这里用了将法线贴图作为遮罩进行遮罩高光来实现具体效果,还有一种方法我很喜欢的效果,再风之旅人的GDC上提过,他们用在了沙漠上,但在海水上也很有效果。风之旅人的沙漠实现方法。这篇文章的实现思路也很值得学习。

Shader入门—6.透明度测试与阴影

首先在进行代码编写前,我们要了解渲染的一个机制,就是Tas序列,给Subshader下添加标签可以定义整个Subshader的队列,给Pass添加标签可以定义该Pass的光照模式比如forwardbase,forwardadd类似。 还要我们需要加深一个概念是渲染顺序。在之前的案例由于是单个光照模型不用考虑物体的遮罩情况,所以无所谓渲染顺序。而对于不透明的物体(Rendertype=opaque)不需要考虑渲染顺序也能在屏幕上得到正确的渲染,这是因为摄像机自己做了的个深度检测,在深度缓冲(z-buffer)中会决定那个物体会被渲染在前面,哪些物体会被遮挡。这时候我们不用去关心渲染顺序,因为摄像机会自动判断。

当我们在进行写透明材质的时候有两种解决方案,一种是透明度测试,一种是透明度混合。

透明度测试:一种极端的检测机制,只要有片元不满足这个测试就会被舍弃,满足就保留然后进行深度测试与深度写入。也就是说,其不用关闭ZWrite。

我们先进行透明度测试的shader书写,在此之前,介绍个clip函数

<pre class="wp-block-syntaxhighlighter-code">void clip(float4 x)
{
    if (any(x &lt;0))
        discard;
}</pre>

意思就是如果给定一个参数的任何一个分量为负数,就会舍弃当前的像素的输出。那么关键函数就很简单了,拿被检测的a分量减去一个自定义值,只有大于0的才能保存。

<pre class="wp-block-syntaxhighlighter-code">Shader "Custom/AlphaTest"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Main Tex", 2D) = "white" {}
        _AlphaValue ("AlphaValue", Range(0,1)) = 0.5
        
    }
    SubShader
    {
        Tags{"Queue"="AlphaTest" "IgnoreProjector"="True" "Rendertype"="TransparentCutout" }
        Pass{
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag 
            #include "Lighting.cginc"
            #include "UnityCG.cginc"
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _AlphaValue;
            struct a2v{
                float4 vertex:POSITION;
                float3 normal:NORMAL;
                float4 texcoord:TEXCOORD0;
            };
            struct v2f{
                float4 pos:SV_POSITION;
                float3 worldNormal:TEXCOORD0;
                float3 worldPos:TEXCOORD1;
                float2 uv:TEXCOORD2;
            };
            v2f vert(a2v v){
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.worldNormal=UnityObjectToWorldNormal(v.normal);
                o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
                o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
                return o;
            }
            fixed4 frag(v2f i):SV_TARGET{
                fixed3 worldNormal=normalize(i.worldNormal);
                fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed4 texColor=tex2D(_MainTex,i.uv);
                clip(texColor.a-_AlphaValue);
                fixed3 albedo=texColor.rgb*_Color;
                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
                fixed3 diffuse=_LightColor0.rgb*albedo*max(0,dot(worldNormal,worldLightDir));
                return fixed4(ambient+diffuse,1.0);
            }
            ENDCG
        }
       
       
    }
    FallBack "Tranaparent/Cutout/VertexLit"
}
</pre>
alphatest的暴力透明

第二种方法要稍微圆滑一点,称为透明度混合,这种方法可以真正的得到透明效果。他会先与存储在颜色缓冲中的颜色进行混合,得到新的颜色。但因为,我们关闭了深度写入,所以我们要注意渲染顺序。

为了进行混合,我们需要了解Blend命令,该命令一般在Pass内

Blend Off关闭混合(默认)
Blend SrcFactor DstFactor片元产生的颜色乘以SrcFactor,加上屏幕上已有的颜色乘以DstFactor,得到最终的颜色(写入颜色缓存)
Blend SrcFactor DstFactor,SrcFactorA DstFactorA同上,只不过使用单独的银子SrcFactorA与DstFactorA来混合透明度通道
BlendOp BlendOperation用其他的操作来取代加法混合
BlendOp OpColor,OpAlpha同上,只不过对于透明度通道的不同操作
常见的混合命令

混合操作(BlendOp)

Add加分
FinalColor=SrcFactor*SrcColor+DstFactor*DstColor
Sub减法(源-目标):
FinalColor=SrcFactor*SrcColor-DstFactor*DstColor
RevSub减法(目标-源):
FinalColor=DstFactor*DstColor-SrcFactor*SrcColor
Min较小值(逐个通道比较)
Max较大值(逐个通道比较)
混合操作

混合因子

One混合因子1,表示完全的源颜色或目标颜色
Zero混合因子0,舍弃掉源颜色或目标颜色
SrcColor源颜色值
SrcAlpha源透明度
DstColor目标颜色
DstAlpha目标透明度
OneMinusSrcColor1-SrcColor
OneMinusSrcAlpha1-SrcAlpha
OneMinusDstColor1-DstColor
OneMinusDstAlpha1-DstAlpha
混合因子

常用的混合命令

<pre class="wp-block-syntaxhighlighter-code">Blend SrcAlpha OneMinusSrcAlpha // 传统透明度
Blend One OneMinusSrcAlpha // 预乘透明度
Blend One One // 叠加
Blend OneMinusDstColor One // 柔和叠加
Blend DstColor Zero // 相乘——正片叠底
Blend DstColor SrcColor // 两倍相乘</pre>

我们在这里运用Blend SrcAlpha OneMinusSrcAlpha,然后再关闭深度写入,其他的就跟我们之前写的贴图材质一样,再复习一遍贴图材质的反射公式:Diffuse=Clight*Mdiffuse*max(0,dot(n,l))所以我们先搭建贴图的shader代码

当然我们想通过一个参数来调节透明度,可以直接在贴图的alpha通道乘以一个参数就行了,然后再使参数暴露。

fixed4 texColor=tex2D(_MainTex,i.uv);
texColor.a*_AlphaScale;</pre>

以下是完整代码

Shader "Custom/AlphaBlendShader"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Main Texture", 2D) = "white" {}
        _AlphaScale ("AlphaScale", Range(0,1)) = 0.5
    }
    SubShader
    {
        Tags{"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
        Pass{
            Tags{"LightMode"="ForwardBase"}
            ZWrite Off 
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            #pragma vertex vert 
            #pragma fragment frag 
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _AlphaScale;
            struct a2v{
                float4 vertex:POSITION;
                float3 normal:NORMAL;
                float4 texcoord:TEXCOORD0;
            };
            struct v2f{
                float4 pos:SV_POSITION;
                float3 worldNormal:TEXCOORD0;
                float3 worldPos:TEXCOORD1;
                float2 uv:TEXCOORD2;
            };
            v2f vert(a2v v){
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.worldNormal=UnityObjectToWorldNormal(v.normal);
                o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
                o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
                return o;
            }
            fixed4 frag(v2f i):SV_TARGET{
                fixed3 worldNormal=normalize(i.worldNormal);
                fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed4 texColor=tex2D(_MainTex,i.uv);
                fixed3 albedo=texColor.rgb*_Color;
                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
                fixed3 diffuse=_LightColor0.rgb*ambient*max(0,dot(worldNormal,worldLightDir));
                return fixed4(diffuse+ambient,texColor.a*_AlphaScale);
            }
            ENDCG
        }
    }
    FallBack "Transparent/Cutout/VertexLit"
}

代码几乎与之前的漫反射贴图的一样,只是添加了 关闭深度写入 与 混合模式,并定义了渲染队列。

AlphaBlend

虽然我们完成了这部分的透明度,但信息的你们一定会发觉这部分透明是有问题的,因为我们看不到物体的内部,只是将整体进行透明化的处理。这时候我们就需要思考为什么会发生这种情况,只能看到整个物体的正面,透明物体在现实中应该不是如此。这是因为我们在AlphaBlend中关闭了深度写入,所以摄像机会自动剔除物体的背面,而在unity中如何判断一个面片的正反呢,其实他是根据法线来判断的,举个例子,当三个点进行连线的时候,顺时针为正,反之则为负。

Unity中如何判断一个面片的正反

所以在背面的面片由于被摄像机判断为反,则被剔除了。不信的话可以随便新建一个Plane然后看看他的法线方向,当他为背面的时候会摄像机则不会渲染该面片。这也是为何在AlphaTest中为何开启了深度写入,但背部面片仍然不被渲染的原因。所以在AlphaTest中如何渲染背部就非常简单,只需在Pass下CGPROGRAM前添加Cull Off命令,就可以关闭剔除功能,从而达到渲染背面的效果。

关闭了剔除功能的AlphaTest

但当我们来到AlphaBlend的透明渲染的时候,我们该如何进行背部渲染呢,因为为了达到Blend的效果,我们需要关闭深度写入,但关闭深度写入摄像机则无法检测到背部信息,所以会有第一次AlphaBlend的渲染效果。在这里我们有一个小trick可以做,正如我们在Toon-Shading中所做的一样,我们先进行一次背部渲染,再进行正面渲染就可以达到我们的效果了,但值得注意的是,由于我们关闭了深度检测所以渲染顺序就很重要。以上的渲染思路的伪代码如下

...
Pass{
   ...
   Cull Front
   ...
}
Pass{
   ...
   Cull Back
   ...
}

所以我们完整的AlphaBlend渲染代码如下

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Custom/AlphaBlendShader"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Main Texture", 2D) = "white" {}
        _AlphaScale ("AlphaScale", Range(0,1)) = 0.5
    }
    SubShader
    {
        Tags{"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
        Pass{
            Tags{"LightMode"="ForwardBase"}
            ZWrite Off 
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Front
            CGPROGRAM
            #pragma vertex vert 
            #pragma fragment frag 
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _AlphaScale;

            struct a2v{
                float4 vertex:POSITION;
                float3 normal:NORMAL;
                float4 texcoord:TEXCOORD0;
            };

            struct v2f{
                float4 pos:SV_POSITION;
                float3 worldNormal:TEXCOORD0;
                float3 worldPos:TEXCOORD1;
                float2 uv:TEXCOORD2;
            };

            v2f vert(a2v v){
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.worldNormal=UnityObjectToWorldNormal(v.normal);
                o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;

                o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
                return o;


            }

            fixed4 frag(v2f i):SV_TARGET{
                fixed3 worldNormal=normalize(i.worldNormal);
                fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed4 texColor=tex2D(_MainTex,i.uv);

                fixed3 albedo=texColor.rgb*_Color;
                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;

                fixed3 diffuse=_LightColor0.rgb*ambient*max(0,dot(worldNormal,worldLightDir));

                return fixed4(diffuse+ambient,texColor.a*_AlphaScale);
            }
            ENDCG
        }
        Pass{
            Tags{"LightMode"="ForwardBase"}
            ZWrite Off 
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Back
            CGPROGRAM
            #pragma vertex vert 
            #pragma fragment frag 
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _AlphaScale;

            struct a2v{
                float4 vertex:POSITION;
                float3 normal:NORMAL;
                float4 texcoord:TEXCOORD0;
            };

            struct v2f{
                float4 pos:SV_POSITION;
                float3 worldNormal:TEXCOORD0;
                float3 worldPos:TEXCOORD1;
                float2 uv:TEXCOORD2;
            };

            v2f vert(a2v v){
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.worldNormal=UnityObjectToWorldNormal(v.normal);
                o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;

                o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
                return o;


            }

            fixed4 frag(v2f i):SV_TARGET{
                fixed3 worldNormal=normalize(i.worldNormal);
                fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed4 texColor=tex2D(_MainTex,i.uv);

                fixed3 albedo=texColor.rgb*_Color;
                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;

                fixed3 diffuse=_LightColor0.rgb*ambient*max(0,dot(worldNormal,worldLightDir));

                return fixed4(diffuse+ambient,texColor.a*_AlphaScale);
            }
            ENDCG
        }
    }
    FallBack "Transparent/Cutout/VertexLit"
}
最终的渲染效果

以上我们的透明块的渲染告一段落。

接着我们进行阴影的绘制,在绘制阴影的时候我们需要再次回顾一下我们之前提到的三个阴影宏,SHADOW_COORDS、TRANSFER_SHADOW、SHADOW_ATTENUATION。首先SHADOW_COORDS只是一个声明,因为顶点数据中并没有阴影的坐标需要通过顶点着色器进行计算,所以SHADOW_COORDS是在v2f结构体中进行声明并在在顶点着色器中进行计算所以,TRANSFER_SHADOW在顶点着色器中计算上一步的声明的阴影纹理坐标。所以,在写这部分代码的时候要注意带有SHADOW_COORDS的结构体应该在顶点着色器的前部,而不能像之前那样,将V2F结构体放在顶点着色器之后。SHADOW_ATTENUATION则是计算阴影的衰减,所以这部分基本上是在片元着色器之中。而这三个宏是包含在AutoLight.cginc之中,所以在计算阴影的时候需要包含这个头文件。但当SHADOW_COORDS与TRANSFER_SHADOW实际上没有任何作用的时候,SHADOW_ATTENUATION的值会直接等于1。

更需要的注意的是,由于这些宏中会使用上下文变量来进行计算,比如SHADOW_COORDS会用v.vertex或者a.pos来计算,所以为了能够这些宏正常运行,我们需要保证自定义的变量名。所以a2v结构体中的顶点坐标变量名必须是vertex,顶点这所去的输入a2v必须命名为v,v2f中顶点位置必须命名为pos。

在AlphaTest中我可以直接添加相关代码

struct v2f{
...
SHADOW_COORDS(3)
...
};

这是v2f的坐标声名,这里注意下,由于我们在这用的是第四套纹理坐标,所以内部写3,还有就是这个宏不需要添加”;”

v2f vert(a2v v){
...
TRANSFER_SHADOW(o);
...
return o;
}

此处我们将上一步声明的坐标点转换成阴影坐标作为输出。这个宏要加”;”

fixed4 frag(v2f i):SV_Target{
...
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
...
return fixed4 (ambient,diffuse*atten,1.0)
}

在此处我们用了UNITY_LIGHT_ATTENUATION这个宏,输入三个参数,atten我们代码中并没有声明类型,这个宏会自动帮我们声明,参数i是我们整个片元着色器的输入参数,还有与其相关的世界坐标。这个宏的计算过程可以在AutoLight.cginc看到,它会将第二个参数传递给SHADOW_ATTENUATION,来计算阴影值。

添加阴影后的alphatest渲染效果

接下来我们在我们的混合透明中每一个pass里加入上述代码,就能得到混合透明的阴影。

Shader入门—EX UE4中的Toon-shading

UE4中实现卡通材质着色有点麻烦,毕竟他不能像U3D那样直接写shader。所以我们先实现屏幕后渲染的调节,这部分实力有点多,主要进行的是场景的描边操作。思路是在屏幕上进行扫描再进行深度检测,不同深度的会进行描边处理。这部分有点麻烦,我想要同一深度也有描边效果。关于这点,我还在学习UE4的shader,希望能达成像U3D中那样的描边材质,对场景中单个物体的轮廓线进行编辑。不然自由度太低了。不过一般场景用用应该是够了。不过本文使用了一个小trick让其可行了。

具体制作过程参照的是UE4直播中所展现出的制作思路。

PostProcessingOutline
深度检测
法线检测
阴影强度
最后输出
根据教程所制作的材质的效果

上面是根据教程所做的效果,可以看出我们不需要在他的skybox中进行检测,所以,我们需要进行一个mask,将skybox剔除。

描边剔除skybox

设置好这部分的nodes后,我们可以在材质预览器中预览该效果

剔除之后的材质球

编辑完之后,我们创建一个材质实例,然后赋予PostProcessVolumes中的渲染功能下的后期处理材质。

PostProcessVolumes下的后期处理材质标签

接着我们选择需要处理描边的物体然后打开自定义深度检测。直接在命令面板搜索custom便可找到,也可以在渲染-渲染自定义深度通道,将其开启,便能使用我们的轮廓线材质。

自定义深度通道

以上是描边shader的基本处理,然后配合我们在unity3d中实现的Toon-Shading中的第二个Pass思路来完成我们的UE4diffuse材质编辑。直接将卡通材质球赋予物体,便可达成我们需要的Toon-Shading的需求

这种纯用材质编辑器进行深度检测生产外轮廓的做法实在是繁琐,而且无法进行单个物体的轮廓修改,最好还是在UE4的HLSL中进行编写比较好,但一般小的项目用这种方法可以节省不少开发陈本,就是连线太烦了。没有coding那样有自由性。

Shader入门—5.顶点动画与UV动画

我们先来写顶点动画的shader。在写这个shader的时候我们需要从我们之前写的纹理贴图的那个shader开始进行修改。还记得我们们的纹理贴图最重要的函数么,就是顶点着色器重的TRANSFROM_TEX(uv,Tex)与片元着色器中的tex2d(Tex,uv);第一个保证了贴图在物体上的正确呈现,第二个保证了在屏幕中正确呈现的效果所以我们的基本架构可以这样

<pre class="wp-block-syntaxhighlighter-code">Shader "Custom/VertexAnimationShader"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Main Tex", 2D) = "white" {}
        
    }
    SubShader
    {
        Pass{
            CGPROGRAM
            #pragma vertex vert 
            #pragma fragment frag 
            #include "UnityCG.cginc"
            sampler2D _MainTex;
            float4 _MainTex_St;
            
            struct a2v {
                float4 vertex:POSITION;
                float2 texcoord:TEXCOORD0;
            };
            
            struct v2f {
                float4 pos:SV_POSITION;
                float2 uv:TEXCOORD0;
            };
           
           
            v2f vert(a2v v){
                v2f o;
                o.pos=UnityViewToClipPos(v.vertex);
                o.uv=v.texcoord;
                return o;
                
            }
          
          
            fixed4 frag(v2f i):SV_TARGET{
                return tex2D(_MainTex,i.uv);
            }
            ENDCG
        }
        
    }
    FallBack "Diffuse"
}</pre>

以上是我们的贴图基础shader。下面就是要思考我们需要什么,我们想呈现一中什么样的运动。在这里我们想要进行想有周期性的运动,随时间的变化而规律变化。所以我们在这时候应该要想到我们初中所学的sin函数。完美符合这个条件。

二维状态下的sin函数运动规律

我们将其放在三维空间中

在三维空间下的sin函数。

这时候我们要引入一个内置参数跟两个内置函数,分别是_Time,distance,sin这三个内置函。首先是_Time这个参数他的xyzw的分量上分别是t/20,t,t*2,t*3这四个内置时间,开始时间为程序开始运行的时间,单个参数类型是float,而函数distance(a,b)返回的值是a,b两点的距离,sin(x)返回的则是在sin函数上的值。所以我们只需要将sin与_Time.y相结合就能得到我们需要的效果。一个简单的方法就是计算每个点到中心的距离再乘以某个值来控制频率,加上_Time.z,最后乘以一个书达到控制幅度的效果。我们暂时不评价这个方法好不好,先实现,然后我们再修改。

首先根据这个思路我们写一下伪代码

dis=distance(v.vertex.xyz,float3(0,0,0));
h=sin(dis*_Frequence+_Time.z)*_Magnitude

所以我仅仅需要改变其顶点运动轨迹便可

Shader "Custom/VertexAnimationShader"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Main Tex", 2D) = "white" {}
        _Frequence("Frequence",Range(0,8))=2
        _Magnitude("Magnitude",Range(0,8))=0.1
        
    }
    SubShader
    {
        Tags{"RenderType"="Qpaque"}
        Pass{
            CGPROGRAM
            #pragma vertex vert 
            #pragma fragment frag 
            #include "UnityCG.cginc"
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _Frequence;
            float _Magnitude;
            
            struct a2v {
                float4 vertex:POSITION;
                float2 texcoord:TEXCOORD0;
            };
            
            struct v2f {
                float4 pos:SV_POSITION;
                float2 uv:TEXCOORD0;
            };
           
           
            v2f vert(a2v v){
                v2f o;
                float dis=distance(v.vertex.xyz,float3(0,0,0));
                float h=sin(dis*_Frequence+_Time.z)*_Magnitude;
                o.pos=mul(unity_ObjectToWorld,v.vertex);
                o.pos.y=h;
                o.pos=mul(unity_WorldToObject,o.pos);
                o.pos=UnityObjectToClipPos(o.pos);
               
               
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
            
                return o;
                
            }
          
          
            fixed4 frag(v2f i):SV_TARGET{
                
                fixed4 col=tex2D(_MainTex,i.uv);
                return col;
            }
            ENDCG
        }
        
    }
    FallBack "Diffuse"
}

我们重点来解释下46行47,48行的意义,首先我们想要再世界坐标系中让他做sin 的循环运动,如果在它自身做的话也不是不可以,想做可以试试,挺有意思的。所以我们首先将物体的顶点坐标转换至世界坐标系中,然后我们将世界坐标中的y分量换成我们的变换后的分量h,再将变换转会到他的自身坐标。最后将其整体输出到裁剪屏幕。

接着我们进行UV动画的shader制作其实思路是一样的,上述的顶点动画是通过_Time改变顶点坐标的xyz分量,同理我们只要在片元着色器中修改UV的坐标分量就行了,

float2 offset=float2(0,0);
offset.x=_Time.y*_Speed;
offset.y=_Time.y*_Speed;
fixed4 tex=tex2D(_MovingTex,i.uv+offset);
Shader "Custom/VertexAnimationShader"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Main Tex", 2D) = "white" {}
        _SubTex("Sub Tex"2D)="white"{}
        _Speed("Speed",Range(0,8))=2
        
    }
    SubShader
    {
        Tags{"RenderType"="Qpaque"}
        Pass{
            CGPROGRAM
            #pragma vertex vert 
            #pragma fragment frag 
            #include "UnityCG.cginc"
            sampler2D _MainTex;
            sampler2D _SubTex;
            float4 _SubTex_ST;
            float4 _MainTex_ST;
            float _Speed;
            
            struct a2v {
                float4 vertex:POSITION;
                float2 texcoord:TEXCOORD0;
            };
            
            struct v2f {
                float4 pos:SV_POSITION;
                float2 uv:TEXCOORD0;
            };
           
           
            v2f vert(a2v v){
                v2f o;
                o.pos=UnityObjectToClipPos(o.pos);
               
               
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
            
                return o;
                
            }
          
          
            fixed4 frag(v2f i):SV_TARGET{
                float2 offset=float2 (0,0);
                offset.x=_Time.y*_Speed;
                offset.y=_Time.y*_Speed;
                fixed4 lightcol=tex2D(_SubTex,i.uv+offset);
                
                fixed4 col=tex2D(_MainTex,i.uv);
                return col+lightcol ;
            }
            ENDCG
        }
        
    }
    FallBack "Diffuse"
}

..最近才知道一个噩耗,就是unity自己的HRDP跟UE4一样无法在操作面修改管线。那我为什么不去修改UE4的管线呢。。shadergraph跟UE4的材质编辑器我依然是能做就做。但总觉得U3D吃枣药丸。唯一的优势都尼玛放弃了。

Shader入门—4.卡通材质(2.正式制作Toon-Shading)

我们来回顾下如何制作卡通材质,我们用两个Pass来实现,一个Pass用来渲染背部来达到描边的效果,一个Pass用来渲染正面达到颜色着色的效果。这其中需要用到一个指令Cull Front与Cull Back,这两个指令可以告诉GPU我渲染的时候不需要渲染那一部分,所以叫做剔除指令。

首先我们来制作Cull Front部分的渲染,在渲染轮廓线的时候,我们可以进行法线描边,但这时候有个问题,模型的法线的深度有区分,如果不对他进行重置,我们得到的轮廓线会有断线的情况。所以在进行描边之前我们需要进行法线的深度统一化,这一步也很简单,直接进行normali.z=-0.5就可以统一描边,为什么是-的呢,这里我们要再次复习下左手坐标系,

左手坐标系

Z轴代表我们的深度坐标,越远z的值越大,而我们的卡通材质需要我们清晰的看到他的描边防止被正面渲染的部分给遮挡住所以进行统一的-0.5的定值。所以我们正式来编写描边的Pass。

Shader"Custom/Toon-Shading"{
    Properties{
        _Outline("Outline Size",Range(0,1))=0.5
        _OutLineColor("OutLine Color",Color)=(1,1,1,1)
        _Color("Color Tint",Color)=(1,1,1,1)
    }
    Subshader{
        Pass{
            NAME "OUTLINE"
            Cull Front
            CGPROGRAM
            
            
            #pragma vertex vert
            #pragma fragmengt frag
            #include "Lighting.cginc"
            #include "UnityCG.cginc"
            struct a2v{
                float4 vertex:POSITION;
                float3 normal:NORMAL;
            };
            struct v2f{
                float4 pos:SV_POSITION;
            };
            v2f vert(a2v v){
                v2f o;
                float4 pos=UnityObjectToClipPos(v.vertex);
                float3 normal=mul((float3x3)UNITY_MATRIX_IT_MV,v.normal);
                normal.z=-0.5;
                pos=pos+float4(normalize(normal),0)*_Outline;
                o.pos=mul(UNITY_MATRIX_P,pos);
                return o;
            }
            fixed4 frag(v2f i):SV_Target{
                    return float4(_OutLineColor.rgb,1);
            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

上述代码中,我标亮的地方需要特别注意下,首先是Cull Front,前文已经说明了,这部分Pass我们专门给她命名成OUTLINE,以后如果写其他这种需要描边的效果可以直接使用UsePass”Toon-Shading/OUTLINE”,注意下,unity中会把有名称的Pass的名称转成全部大写,所以我们在命名的时候直接命名成大写,这样就不太会忘记这种小细节。

然后float4 这个定义,这里有一个豆知识,明明三个数字就能表达点的坐标,为什么要用float4来定义,一个原因是我们在变换的时候用的矩阵是4X4的矩阵,所以为了达到转换效果西药将他们转换到一个齐次坐标的空间进行转化。这个是转换时的意义,还有就是当(x,y,z,1)时代表一个点,而(x,y,z,0)时代表一个向量。所以我们在将顶点位置向外延申的时候除了要normalize还需要将他补齐为一个4X4的向量。

接下来我们来实现真正关键的光照模型所在Pass。在这我们再次回顾一下Blinn-Phong模型,

Specular=Clight*Mdiffuse*max(0,n·halfDir)^_gloss

写成代码就是

fixed4 specular=_LightColor0.rgb*_Diffuse*pow(max(0,dot(normal,halfDir)),_Gloss);

其中halfDir相信大家还没有忘记,就是viewDir+lightDir也就是视角向量+入射光线向量,其中视角向量可以用_WorldSpaceCameraPos0.xyz-UnityObjectToWorldNormal(normal)来得到。这里稍微复习一下Blinn-Phong的高光模型,虽然我们不会在这个shader中用它,但将他修改下,就能够为我们所用。

我们照样需要normal与halfDir的点积,但我们需要的不是渐变的高光,而是用它的点积跟一个阈值比较。这时候我们要用一个CG函数step(a,x)这是一个比较函数,当x大于a的时,返回1否则返回0,所以这部分我们可以这么写,

float spec=dot(worldNormal,halfDir);
spec=step(a,spec);

但我们使用这个进行着色的时候,会发现表面锯齿有点大,所以我们引入smoothstep(-w,w,spec-a) smoothstep(a,b,c)的数学意义是,当c小于a的时候返回0,大于b的时候返回1在这之间的时候进行插值,我们结合这两个函数看,其实smoothstep是step的进化版,step是直接拿spec-a,当其>0的时候返回1,<0的时候返回0,所以spec-a的时候无外乎有三种情况,<-w,>w,与在-w,w之间,而这个w有我们来定义,在本例中我们会用到fwidth函数,来得到临近像素之间的近似导数。

所以我们来写这部分的shader。首先我们在达成这个效果的时候需要什么?毋庸置疑的表面颜色_Color,_Specular,_SpecularScale,这三个是高光模型中必备的,然后包含我们上面一个pass所需要的_Outline与_OutlineColor,

Shader "Custom/Toon-ShadingShader2"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _Specular("Specular Color",Color)=(1,1,1,1)
        _SpecularScale("Specular Scale",Range(0,0.1))=0.1
        _Outline("Outline Size",Range(0,1))=0.5
        _OutlineColor("Outline Color",Color)=(1,1,1,1)
        _MainTex("Main Tex",2D)="white"{}
    }
    SubShader
    {
        Tags{"RenderType"="Opaque"  "Queue"="Geometry"}
        
        Pass{
            
            
            NAME "OUTLINE"
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag 
            #include "Lighting.cginc"
            #include "UnityCG.cginc"

            float _Outline;
            fixed4 _OutlineColor;

            struct a2v{
               float4 vertex:POSITION;
               float3 normal:NORMAL;

            };

            struct v2f{
                float4 pos:SV_POSITION;
                
            };

            v2f vert(a2v v){
                v2f o;
                float4 pos=UnityObjectToClipPos(v.vertex);
                float3 normal=mul((float3x3)UNITY_MATRIX_IT_MV,v.normal);
                normal.z=-0.5;
                pos=pos+float4(normalize(normal),0)*_Outline;
                o.pos=mul(UNITY_MATRIX_P,pos);
                return o;
                
            }

            fixed4 frag(v2f i):SV_TARGET{
                return float4(_OutlineColor.rgb,1.0);
            }
            
            ENDCG
        }

        Pass{
            Tags{"LightMode"="ForwardBase"}

            Cull Back

            CGPROGRAM
                ...
            ENDCG
        }

    }
    FallBack "Diffuse"
}

这个就是我们的大体框架,然后我们开始编写代码的初始工作,首先我们的#pragma vertex与#pragma fragment不用谈都要的,然后我们这里要用到#pragma multi_compile_fwdbase这个组包含了所有向前渲染的关键字,但是这个只能处理平行光。如果缺少这段代码的话,unity会默认编译

DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_ON SHADOWS_OFF

接下来是头文件,unityCG.cginc与Lighting.cginc必不可少。由于我们需要计算阴影,所以还需要AutoLight.cginc,这里要额外添加一个UnityShaderVariables.cginc,具体的含义可以在他的头文件中获取。

                        #pragma vertex vert 
            #pragma fragment frag 
            #pragma multi_compile_fwdbase

            #include "UnityCg.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"
            #include "UnityShaderVariables.cginc"

接下来就是定义我们需要的参数了。

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _Specular;
            fixed _SpecularScale;

这些基本定义就不再赘述,接着写我们的顶点数据到顶点着色器的结构体

            struct a2v{
                float4 vertex:POSITION;
                float3 normal:NORMAL;
                float4 texcoord:TEXCOORD0;
                
            };

我们需要从顶点数据那边拿到的首先是就是顶点坐标与顶点法线,还有uv坐标,由于我们输出到片元着色器中也需要uv所以们在a2v中用texcoord作为顶点UV的参数,并附上第一套坐标值。

            v2f vert(a2v v){
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
                o.worldNormal=UnityObjectToWorldNormal(v.normal);
                o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;

                TRANSFER_SHADOW(o);

                return o;
            }

这里我们解释两个宏,一个是TRANSFORM_TEX(a,b),关于这个解释Unity的官方给出的解释如下vertex program uses the TRANSFORM_TEX macro from UnityCG.cginc to make sure texture scale and offset is applied correctly, and fragment program just samples the texture and multiplies by the color property.翻译一下就是顶点着色器使用这个宏可以确保应用纹理的比例与偏移,但片元着色器中只需要通过与颜色的相乘就能达到采样的结果。就是说当对顶点进行纹理着色的时候,使用这个宏可以正确的进行着色不至于使他的坐标进行偏移。

接下来是TRANSFER_SHADOW(o);这个宏其实是对应下一个v2f的结构体中的SHADOW_COORDS()来使用的,他将SHADOW_COORDS()声明的阴影坐标转换成片元着色器中所需要的阴影坐标,供其着色。所以下面我们来进行struct v2f的编写

            struct v2f {
                float4 pos:SV_POSITION;
                float2 uv:TEXCOORD0;
                float3 worldNormal:TEXCOORD1;
                float3 worldPos:TEXCOORD2;
                SHADOW_COORDS(3)
            };

这部分代码中的关键在前文已经解释过了,SHADOW_COORDS()仅起到一个声明阴影坐标的作用。接下来是我们最重要的片元着色器。首先我们再来回顾下我们需要什么,Blinn-Phong模型中我们需要表面法线点乘halfDir,所以我们需要世界坐标下的法线,视角向量,入射光向量。与之相对的halfDir。所以我们继续初始的声明计算

            fixed4 frag(v2f i):SV_TARGET{
                float3 worldNormal=normalize(i.worldNormal);
                float3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
                float3 worldViewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
                float3 halfDir=normalize(worldLightDir+worldViewDir);

                
            }

虽然本案例不用进行贴图,但以防万一,我们仍在偏远着色其中进行贴图着色,按照之前的贴图纹理,我们了解了一个函数tex2D(tex,uv)并乘上我们的_Color进行一个正片叠加的效果,得到我们的反射率效果

fixed3 tex=tex2d(_MainTex,i.uv);
fixed3 albedo=tex.rgb*_Color;

接着我们获取环境光通过UNITY_LIGHTMODEL_AMBIEN.xyz乘上albedo

fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;

然后我们进行漫反射的计算,在这里为了使正面颜色更加突出,我们可以用一个新的模型半兰伯特漫反射模型(half-Lambert diffuse model),这是一个效果模型,跟现实中的反射模型没有一点关系,我们回忆一下我么的兰伯特模型的计算公式diffuse=clight*mdiffuse*max(0,n*l) 其中max(0,n*l)我们再代码中是这样体现的

saturate(0,dot(worldNormal,worldLightDir));

而我们的半兰伯特模型,则是将其乘以0.5并加上0.5的偏移这其中的代码我们可以这么写

diff=dot(worldNormal,worldLightDir);
diff=0.5*diff+0.5;

如果这时候我们直接乘上_LightColor0.rgb与_Diffuse就可以直接获得我们的半兰伯特漫反射模型。但因为这个案例中我们需要进行阴影的计算,我们来引入一个新的宏UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos)这个宏可以帮我们计算阴影的衰减,atten不用声明,这个宏会自动声明,第二个参数是前面结构体所取得的阴影坐标,用来计算阴影值,后面的是在世界空间坐标系啊的坐标,用来计算光源下的坐标。

UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);

我们再将其得到的atten与半兰伯特公式相乘,得到其阴影部分的阴影衰减。

diff=(0.5*diff+0.5)*atten;

在我们得到这个阴影衰减后可以用一张渐变图对其采样,这样我们在效果上可以实现渐变阴影的衰减,这时候我们在头部属性,增加_Ramp(“Ramp Texture”,2D)=”whiter”{},并定义与之相对应的变量sampler2D _Ramp;float _Ramp_ST;

Shader"Custom/Toon-Shading"{
Properties{
...
_Ramp("Ramp Texture",2D)="white"{}
...
}
Subshader{
...
sampler2D _Ramp;
float _Ramp_ST;
...

fixed4 diffuse=_LightColor0.rgb*albedo*tex2D(_Ramp,float2(diff,diff)).rgb;

这样我们的漫反射部分就已经结束了。

接着我们进行高光部分的编写,我们来回顾一下,Blinn-Phong的公式

specular=Clight*Mdiffuse*max(0,n*halfDir)^Gloss 然后我们改进后的一部分公式smoothstep(-w,w,spec-a),其中w是我们通过fwidth函数实现的,而fwidth这个函数在CG语言中的含义如下:fwidth — return the sum of the absolute value of derivatives in x and y 意思是返回x,y中导数绝对值的和,但我们只需要临近像素之间的近似导数值,而高光部分我们只需要如此计算:fixed w=fwidth(dot(worldNormal,halfDir))*2则可

所以这部分代码如下

fixed w=fwidth(dot(worldNormal.halfDir))*2.0;
fixed3 specular=_Specular.rgb*lerp(0,1,smoothstep(-w,w,spec+_SpecularScale-1));

这部分为什么没有乘以_LightColor0.rgb呢,因为在卡通渲染的过程,我们希望高光部分不被环境光打扰,但如果你有这部分需求也能乘,但我试过,效果不是那么好,高光部分容易被干扰,从而达不到应有的效果。但如果你这时候你运行代码的时候,将_SpecularScale拉到0你会发现还会有高光反射的光照,这时候该怎么办呢,其实很简单直,运用我们的step函数将_SpecularScale与一个极小的值进行比较,当_SpecularScale等于0的时候返回0,修改过后的高光部分的代码如下

fixed3 specular=_Specular.rgb*lerp(0,1,smoothstep(-w,w,spec+_SpecualrScale-1))*step(0.00001,_SpecularScale);

如此我们边完成了高光部分的代码工作,最后我们将环境光、漫反射、高光相加就可以了,如果你要实现自发光的效果,可以直接定义一个_Emission的Color,然后与结果的和相乘便能达到效果。

Shader "Custom/Toon-ShadingShader2"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _Specular("Specular Color",Color)=(1,1,1,1)
        _SpecularScale("Specular Scale",Range(0,0.1))=0.1
        _Outline("Outline Size",Range(0,1))=0.5
        _OutlineColor("Outline Color",Color)=(1,1,1,1)
        _MainTex("Main Tex",2D)="white"{}
        _Ramp("Ramp Texture",2D)="white"{}
    }
    SubShader
    {
        Tags{"RenderType"="Opaque"  "Queue"="Geometry"}
        
        Pass{
            
            
            NAME "OUTLINE"
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag 
            #include "Lighting.cginc"
            #include "UnityCG.cginc"
            float _Outline;
            fixed4 _OutlineColor;
            struct a2v{
               float4 vertex:POSITION;
               float3 normal:NORMAL;
            };
            struct v2f{
                float4 pos:SV_POSITION;
                
            };
            v2f vert(a2v v){
                v2f o;
                float4 pos=UnityObjectToClipPos(v.vertex);
                float3 normal=mul((float3x3)UNITY_MATRIX_IT_MV,v.normal);
                normal.z=-0.5;
                pos=pos+float4(normalize(normal),0)*_Outline;
                o.pos=mul(UNITY_MATRIX_P,pos);
                return o;
                
            }
            fixed4 frag(v2f i):SV_TARGET{
                return float4(_OutlineColor.rgb,1.0);
            }
            
            ENDCG
        }
        Pass{
            Tags{"LightMode"="ForwardBase"}
            Cull Back
            CGPROGRAM
            #pragma vertex vert 
            #pragma fragment frag 
            #pragma multi_compile_fwdbase
            #include "UnityCg.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"
            #include "UnityShaderVariables.cginc"
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _Specular;
            fixed _SpecularScale;
            sampler2D _Ramp;
            float _Ramp_ST;
            
            struct a2v{
                float4 vertex:POSITION;
                float3 normal:NORMAL;
                float4 texcoord:TEXCOORD0;
                
            };
            struct v2f {
                float4 pos:SV_POSITION;
                float2 uv:TEXCOORD0;
                float3 worldNormal:TEXCOORD1;
                float3 worldPos:TEXCOORD2;
                SHADOW_COORDS(3)
           };
           
            v2f vert(a2v v){
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
                o.worldNormal=UnityObjectToWorldNormal(v.normal);
                o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
                TRANSFER_SHADOW(o);
                return o;
            }
           
            
            fixed4 frag(v2f i):SV_TARGET{
                fixed3 worldNormal=normalize(i.worldNormal);
                fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed3 worldViewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 halfDir=normalize(worldLightDir+worldViewDir);
                
                fixed3 tex=tex2D(_MainTex,i.uv);
                fixed3 albedo=tex.rgb*_Color.rgb;
                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
                fixed diff=dot(worldNormal,worldLightDir);
                UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
                diff=(diff*0.5+0.5)*atten;
                fixed3 diffuse=_LightColor0.rgb*albedo*tex2D(_Ramp,float2(diff,diff)).rgb;
                fixed w=fwidth(dot(worldNormal,halfDir))*2.0;
                fixed3 specular=_Specular*lerp(0,1,smoothstep(-w,w,dot(worldNormal,halfDir)+_SpecularScale-1))*step(0.0001,_SpecularScale);
                return fixed4 (ambient+diffuse+specular,1.0);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

以上就是我们的完整代码了。这部分如何再shadergraph中实现,我明天摸鱼的时候再弄吧。昨天测试了下代码,发现轮廓线有问题,我怀疑是描边pass 的法线的锅,果不出其然,在debug的过程中,发现是unityObjectToClipPos这个内置函数的锅将其换成原先老版的矩阵变换mul((float3x3)UNITY_MARITX_MV,v.vertex)这个是老版的矩阵转换,作用是将顶点坐标转换至裁剪空间内,更新后这个内置矩阵被UnityObjectToClipPos(v.vertex)给取代。具体为何会出现这个情况,有待进一步的分享。

在制作shadergraph的时候再将我们的custom function的代码优化一下。

void LWRPLightingFunction_float (float3 ObjPos, out float3 Direction, out float3 Color, out float ShadowAttenuation )
{
   #ifdef LIGHTWEIGHT_LIGHTING_INCLUDED
   #define LIGHTWEIGHT_LIGHT_INCLUDE
      //Actual light data from the pipeline
      Light light = GetMainLight(GetShadowCoord(GetVertexPositionInputs(ObjPos)));
      Direction = light.direction;
      Color = light.color;
      ShadowAttenuation = light.shadowAttenuation;
      
   #else
   
      //Hardcoded data, used for the preview shader inside the graph
      //where light functions are not available
      Direction = float3(-0.5, 0.5, -0.5);
      Color = float3(1, 1, 1);
      ShadowAttenuation = 0.4;
      
   #endif
}

之前的CG代码与HLSL很类似,所以相当于平铺直叙。将此代码直接复制到txt中然后改文件后缀为hlsl,添加到场景资源中。

创建我们的custom function,这里要注意两个点,首先是Name这里要与代码中的函数名一样,然后输出的三个参数的上下顺序应该与函数中的三个顺序相同,且命名相同。

Shader入门—4.卡通材质(1.Blinn-Phong模型)

1.Blinn-Phong高光模型结合漫反射

前文所写的基本是比较基础的光照模型,有点没意思,我们这次来整点花货,前几年比较流行的卡通材质。首先我们要了解一下卡通材质是与之前的漫反射光照模型不同,卡通材质的光照模型主要是基于色调的光照模型,通过用漫反射系数对纹理进行一张张的采样,来控制漫反射的色调。高光处,我们用一块明显的纯色区域来表示。这时我们需要用到一个高光模型Blinn-Phong模型。除了这些,卡通渲染最具代表性的就是他明显的轮廓线。为了实现这种轮廓线,我们有几种方法,最常用的是通过两个PASS来分别渲染,一个PASS只渲染背面并便宜发现来得到轮廓选,而另外一个只需要渲染正面。

首先我们先了解一下高光模型,传统的高光模型公式如下

Phong高光模型

这个模型跟我上一节所学的漫反射光照模型有点类似,mspecular 是高光区域的颜色,v是视觉方向,r是反射方向mgloss是高光系数,系数越大则亮度越小。当我们获取r的时候,需要通过物体表面的法线单位向量与光源的单位向量通过计算得到

phong模型计算高光

我们的计算公式如下

高光反射的计算公式

如此我们便可以计算高光反射部分。

在这之后,Blinn提出了一个简单的方案来修改这个模型,他提出了一个向量h,他是通过vl取平均然后归一化后得到的

h的计算过程

修改过后的Blinn-Phong高光模型

我们分别来在程序代码中实现这两个高光光照模型。

漫反射部分与上篇类似,这边重点说下高光的代码

fixed3 reflectDir=normalize(reflect(-worldLightDir,worldNormal));
fixed3 viewDir=normalize(_WorldSpaceCammeraPos.xyz-unity_ObjectToWorld(v.vertex).xyz);

这里我们介绍下reflect函数,reflect(i,n),i是指入射方向,n是指法线方向,其中的参数类型可以是float,float2,float3,这里我们通过对光源方向取反,得到他的入射方向,这里取反的原因是,在我们用_WorldSpaceLightPos0这个函数来取得光源方向的时候,他是基于物体的表面反射而得到的值,所以在用入射光源的方向的时候,只需要取反就可以了。worldNormal则是正常通过mul(v.normal,(float3x3)unity_WorldToObject)来计算,这里我们得到的v.normal是基于物体坐标所得到的法线向量,我们用法线空间转换矩阵进行反乘,就能够得到在空间中的法线坐标,同样,法线是一个xyz向量,我们只需要去矩阵的前三行前三列就可以了,然后进行归一化。这里我提一下viewDir的计算过程,简单的来说就是空间相机坐标减去物体坐标,如果不够具象化,我们画一下就能够非常清晰的理解这个几何意义,

viewDir的具象化

其中向量C是空间相机坐标,向量O是空间物体坐标,两个相减,就得到了向量V,也就是我们的视角向量,在对他进行归一化。有了这两个值,我们就可以进行高光的代码编写了

fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(viewDir,reflectDir)),_Gloss);

之后再加上我们上篇所得到diffuse与ambient就能得到一个完整的Phong-高光模型的漫反射模型。

Shader "Custom/DiffuseShader"
{
    Properties
    {
        _Diffuse("Diffuse Color",Color)=(1,1,1,1)
        _Specular("Specular Color",Color)=(1,1,1,1)
        _Gloss("Gloss",Range(8,255))=20
    }
    SubShader
    {
        pass{
            Tags{"LightModel"="ForwardBase"}
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            struct a2v{
                float4 vertex : POSITION;
                float3 normal : NORMAL;

            };

            struct v2f{
                float4 pos:SV_POSITION;
                float3 color:COLOR;
            };

            v2f vert(a2v v){
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);

                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));

fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);

                fixed3 lightNormal=normalize(_WorldSpaceLightPos0);

                fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,lightNormal));

                fixed3 reflectDir=normalize(reflect(-WorldLightDir,worldNormal));

                fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-unity_ObjectToWorld(v.vertex).xyz);

                fixed3 specular=_LightColor0.rgb*_Specular*pow(saturate(dot(viewDir,reflectDir)),_Gloss);
                
                
                o.color=ambient.rgb+diffuse.rgb+specular;



                return o;
            }

            fixed4 frag(v2f i):SV_TARGET{
                return fixed4(i.color,1.0);
            }


            ENDCG
        }
    }
    FallBack "Diffuse"
}

至此我们得到了一个基于phong高光模型的漫反射模型。

同理,我们将Blinn-Phone高光模型进行代码化,首先我们需要什么?再来看一下我们的Blinn-Phong模型公式

n是物体表面法线向量,h是新定义的光源向量与视觉方向取平均后的归一化。在代码中我们可以直接将worldLightDir与viewDir相加然后进行归一化计算就行了。所以这Blinn-Phong的高光部分如下代码呈现

fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-unity_ObjectToWorld(v.normal).xyz);
fixed3 halfDir=normalize(worldLightDir,viewDir);

得到这两个参数我们就可以直接套用公式来计算高光部分了

fixed3 diffuse=_LightColor0.rgb*_Specular*pow(max(0,dot(worldNormal,halfDir)),_Gloss);

所以我们的Blinn-Phong高光漫反射模型圆满了。下面是我们的完整代码:

Shader "Custom/Blinn-PhongSpecularDiffuseShader"
{
    Properties{
        _Diffuse("Diffuse Color",Color)=(1,1,1,1)
        _Specular("Specular Color",Color)=(1,1,1,1)
        _Gloss("Gloss",Range(8,255))=20
    }

    SubShader{
        Pass{
            Tags{"LightMode"="ForwardBase"}

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float  _Gloss;

            struct a2v{
                float4 vertex:POSITION;
                float3 normal:NORMAL;

            };

            struct v2f{
                float4 pos:SV_POSITION;
                fixed3 color:COLOR;
            };

            v2f vert(a2v v){
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);

                fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));

                fixed3 lightDir=normalize(_WorldSpaceLightPos0.xyz);

                fixed3 diffuse=_LightColor0.rgb*_Diffuse*saturate(dot(worldNormal,lightDir));

                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-UnityObjectToWorld(v.normal).xyz);

                fixed3 halfDir=normalize(lightDir+viewDir);

                fixed3 specular=_LightColor0.rgb*_Specular*pow(max(0,dot(worldNormal,halfDir)),_Gloss);

                o.color=ambient+diffuse+specular;

                return o;

            }

            fixed4 frag(v2f i):SV_TARGET{


                return fixed4(i.color,1.0);
            }



            ENDCG




        }

    }

    FallBack "Diffuse"
}

以上是我们完整的Blinn-Phong高光漫反射模型,但注意,这部分代码我们是在顶点着色器中所实现的,在片元着色器中如何实现网上有挺多的资料的,这里就不在赘述,原理跟这个差不多,只是在片元着色器中少了一步世界坐标下的物体法线反转换。

在ShaderGraph与UE4中如何实现BlinnPhong高光漫反射模型。由于时间太晚了。先睡一觉。。。。。明天看看能不能在摸鱼的时候做成。。。

Blinn-Phong高光漫反射模型

这是上午的摸鱼成果。需要注意的是shadergraph可以直接获取viewDirection,而不必像上述代码那样通过_WorldSpaceCameraPos.xyz-unity_ObjectToWolrd(v.normal).xyz 来获取,其他的思路跟代码思路一样,就不再赘述。

Shader入门—3.漫反射光照模型

在构建这个shader之前,我们需要知道一个定律,兰伯特定律(Lambert’s low):反射光线的强度与表面法线和光源方向之间的夹角的cos成正比,因此漫反射的计算公式如下

兰伯特定律

n是表面法线,l是光源的单位矢量,max(0,n·l)是取0,n·l中的最大值,clight 是光源颜色,mdiffuse 是材质的漫反射颜色。

所以当我们要计算漫反射的颜色的时候,我们需要4个参数,物体的表面法线、光源的单位矢量、光源颜色与材质的漫反射颜色。由于本章所编写的是默认光线下的光照模型,所以只进行单一光照的计算,而表面法线与光源的单位矢量可以通过计算取得,所以我们只需定义表面漫反射的颜色即可。所以在属性声明中只需要进行漫反射颜色的定义。以下是我的shader代码。

Shader "Custom/DiffuseShader"
{
    Properties
    {
        _Diffuse("Diffuse Color",Color)=(1,1,1,1)
    }
    SubShader
    {
        pass{
            Tags{"LightModel"="ForwardBase"}
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"

            fixed4 _Diffuse;

            struct a2v{
                float4 vertex : POSITION;
                float3 normal : NORMAL;

            };

            struct v2f{
                float4 pos:SV_POSITION;
                float3 color:COLOR;
            };

            v2f vert(a2v v){
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);

                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));

                fixed3 lightNormal=normalize(_WorldSpaceLightPos0);

                fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,lightNormal));

                o.color=ambient.rgb+diffuse.rgb;

                return o;
            }

            fixed4 frag(v2f i):SV_TARGET{
                return fixed4(i.color,1.0);
            }


            ENDCG
        }
    }
    FallBack "Diffuse"
}

我们继续来逐行翻译,因为我们只是编写了一个漫反射模型,所以暂时不对贴图进行采样。我们在属性中只添加了一个颜色属性所对应的是漫反射颜色。

我们在进行漫反射计算的时候要正确的设置LightMode才能得到我们接下来需要用到的_LightColor0这个内置变量,LightMode指的是灯光的渲染路径,而ForwardBase则是设置灯光的渲染路径的一个标签,它决定了渲染器如何渲染灯光,其他的还有ForwardAdd等标签。接着就是正常的框架搭建,不在赘述,但我们这里运用到了一个头文件”Lighting.cginc”,该头文件主要是有一些内置的函数可以直接供我们调用,比如之前所说的_LightColor0这个变量。

fixed4 _Diffuse;

_Diffuse是颜色属性,我们按照惯例直接定义成fixed4类型。

我们从顶点着色器开始,首先编写结构体a2v,我们要想一想要构建漫反射模型需要从我们的顶点数据中取得什么数据?顶点位置自然不必多说,我们来回顾一下Lambert定律Cdiffuse =Lcolor *Mdiffuse *max(0,n*l)。环境光我们可以通过内置函数得到,漫反射材质的颜色由我们的意愿控制,剩下的就是表面法线与光的单位向量了。而光的单位向量我们可以通过Unity内置的函数取得,而表面法线则需要与顶点进行交互才能够取得所以,我们这里又用到一个语义:NORMAL,获取顶点的法线信息,所以我们的结构体如下

Struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
};

我们将NORMAL语义所取得的顶点法线信息存储在normal这个变量中,又因为法线是一个只包含XYZ的向量信息,所以我以float3来进行定义。

下面我们来构建顶点着色器,

v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
...
}

v2f 定义了我们的输出参数是o,之后的就是一般设置。顶点的法线信息是基于模型坐标的,所以我们首先将其变换到空间坐标中,这里需要将顶点的法线与变换矩阵unity_WorldToObject(老版本为:_World2Object)进行mul计算,之后将其归一化,所以我们这么写

fixed3 worldNormal=normalize(mul(v.normal,unity_WorldToObject));

注意,这么些对么?首先法线是一个三维矢量,而我们的unity_WorldToObject则是一个4X4的矩阵,所以我们在进行变换的时候只需要截取前三行与前三列就行了,修改过后的代码

fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));

这样我们就得到了我们需要的n,接下来我们来进行l的计算,关于光源的方向我们可以直接使用内置函数_WorldSpaceLightPos0来取得,但请注意,这个函数只计算单一函数,所以在更复杂的光线中并不能计算出正真的光线信息,但在这个模型中是够用了。

fixed3 worldLight=normalize(_WorldSpaceLightPos0);

如此我们就得到了我们需要两个向量的归一化,worldNormal与worldLight,接着就很简单了,套入我们的Lambert定律就行了,再回一下Lambert定律:Cdiffuse=Lcolor*Mdiffuse*max(n,l)

fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(worldNormal,worldLight);

至此,我们的漫反射计算结束,但整体的顶点着色器还没有完成,因为在考虑漫反射的时候不得不在意环境光。所以我们还需要调用一个内置函数UNITY_LIGHTMODEL_AMBIENT。

fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;

我们得到了漫反射颜色与环境光,将其相加则是最终我们所得到的漫反射模型颜色,最后返回o就可以了。所以完整的顶点着色器如下

            v2f vert(a2v v){
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);

                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));

                fixed3 lightNormal=normalize(_WorldSpaceLightPos0);

                fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,lightNormal));

                o.color=ambient.rgb+diffuse.rgb;

                return o;
            }

恭喜,我们已经将最困难的部分完成了,接下来我们来编写片元着色器的结构体,首先我们跟之前一样,需要将裁剪过后的顶点坐标传输给pos,我们还仍需要顶一个color变量,color是作为o的输出与片元中的输入,我们可以赋予他COLOR,有些资料也可以赋予TEXCOORD0,这并不是写死的,可自由发挥,所以我们的结构体为

struct v2f{
pos:SV_POSITION;
color:COLOR;
};

接着是我们的片元着色器,这部分很简单,因为我们将所有的工作都在顶点着色其中做完了,所以直接输出顶点信息就可以了

fixed4 frag(v2f i):SV_Target{
return fixed4 (i.color,1.0)
}

注意,由于我们在顶点着色器中输出的颜色是rgb是一个三位信息,所以我们添加个1.0将其补完。

至此,我们完成了我们整个的Lambert光照模型,下一节我们将结合贴图与这个模型进行一个带有漫反射性质的贴图模型。

接下来我们在shader graph实现这个漫反射模型

shader graph中的Lambert漫反射模型

大体思路跟我们写代码的思路一样,但值得提的一点是shader graph中并没有默认光源位置的获取,所以我们要添加一个自定义节点来获取光的颜色以及矢量

自定义节点

#if SHADERGRAPH_PREVIEW
Direction=half3(0.5,0.5,0); Color=1;
#else
Light light=GetMainlight();
Direction=light.direction;
Color=light.color;
#endif

在实际的制作过程中,这个代码非常常用,所以我直接贴出来吧。

接下来我们在UE4中复现我们的操作。SD主要是进行材质的编写,并不涉及管线的编写,所以这篇我们就略过了。

UE4中的shader

Shader入门—EX:BRDF光照模型(在施工)

在我们初中的物理课程有粗线的将结果物体是如何在眼中成像的,我们所看到的物体其实是光打到物体上,物体吸收一部分的光然后发射到我们的眼中,再经由视觉神经进行计算来使之成像。所以这里就有三个参数,光源的位置与方向,视角方向。但一个表面如何进行光照交互的,这其中我们可以用一个光照模型来阐述,这个模型就是BRDF(Bidirectional Reflectance Distribution Function)模型,中文名是双向反射分布函数。

BRDF有两种理解,第一种是当给定入射角度的时候,BRDF可以给出所有出射方向上的反射和散射光线的相对分布情况。注意,BRDF模型并不是真实的光照模型,而是看起来对的光照模型,这是一个经验模型。第二种理解是,当给定观察方向的时候,BRDF可以给出所有入射方向到出射方向的光线分布。

Filament中基于物理的渲染

Shader入门——2贴图材质

本节我们进行材质上最基本的贴图。也就是最常用的贴图材质编写。首先是unity shader的代码编写。

Shader"Custom/BaseTextureMatShader"
{
    Properties{
        _Color("Color Tint",Color)=(1.0,1.0,1.0,1.0)
        _MainTex("Main Texture",2D)="white"
    }
    Subshader{
        Pass{
            CGPROGRAM
            #pragma vertex vert
            #pragma fragmengt frag
            fixed4 _Color;
            sampler2D _MainTex;
            
            float4 vert(float4 pos:POSITION):SV_POSITION{
                return UnityObjectToClipPos(pos);
            }

            fixed4 frag(float2 uv:TEXCOORD0):SV_Target{
                return tex2D(_MainTex,uv);
             }
            
            ENDCG
        }
    }
    Fallback "Diffuse"
}

我们继续来逐行翻译,但也不必像之前那样逐行,我们只翻译类比上一篇代码中的不同部分。首先我们在Properties下增加了_MainTex这个属性,他的面板名称为“Main Tex”,属性是2D贴图,默认是白色。由于我们在这次shader中要调用_MainTex这个自定义属性,所以我们要在Pass中定义它为sampler2D。sampler2D是一个CG中的一中数据类型,主要是用来储存纹理信息的,常见的一般有sampler,sampler2D,samplerCUBE等。我们这里要定义_MainTex所以进行定义

sampler2D _MainTex;
fixed4 _Color;

然后我们正常定义顶点着色器于片元着色器的入口,一般我会直接定义

#pragma vertex vert
#pragma fragmengt frag

我们暂时先不管结构体struct的书写,根据上篇的内容我们应该直接进行顶点着色器的编写,我们顺着这个思路走

float4 vert(float4 vertex:POSITION):SV_POSITION{
    return UnityObjectToClipPos(vertex);
}

这部分跟上篇一样,我们略过翻译,接着我们开始写本篇的新内容,纹理贴图。_MainTex,

fixed4 frag( float2 uv:TEXCOORD0):SV_Target{
    return tex2D(_MainTex,uv);
}

这部分,已经有了区别,首先我们的片元着色器需要输入渲染pipline中三角形遍历后所产生的UV,又因为UV只是一个二维坐标,所以以float2来定义,而TEXCOORD0则是将第一套纹理指定给UV这个参数,让其获得上一步片元坐标参数。之后我们在函数中返回的是一个tex2D的值,tex2D是一个对纹理采样的值,他的第一个参数是需要被采样的值,第二个参数是一个float2的纹理坐标。所以这个过程等于在像素点上进行点对点的涂色。所以按照上一篇的思路,我们的texture shader应该是如下

Shader"Custom/textureShader"{
    Properties{
        _MainTex("Main Tex",2D)="white"{}
        _Color("COlor Tint",Color)=(1,1,1,1)
    }
    Subshader{
        Pass{
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            fixed4 _Color;
            sampler2D _MainTex;

            float4 vert (float4 vertex:POSITION):SV_POSITION{
                return UnityObjectToClipPos(vertex);
            }

            fixed4 frag(float2 uv:TEXCOORD0):SV_Target{
                return tex2D(_MainTex,uv);
            }

            ENDCG
        }

    }
    Fallback "Diffuse"
}

这个shader在unity中没有报错,但呈现的效果不是我们想要的。

我们并不是想要这种纯色的贴图,而是更加丰富的

但这个错误只在5.6之后的版本有,5.6之前的没有,只能说是更新错误的锅,有一种解释是,5.6更新之后一个特性是不能直接在vertex的函数中获取POSITION再返回,必须要从结构体中获取再返回,但我试了试这种说法,依然不行,应该是在vertex与fragment中同时又相同的UV的坐标返回才能有正常的结果,也就是说vertex与fragment中的uv应当匹配才能有正确的结果,所以我们进行结构体的编写,这部分编写与C语言很相似首先是顶点着色器的输入结构体,首先我们要从顶点数据中获得两个数据,一个是我们一定要的顶点坐标,也就是POSITION,另外一个是顶点的UV坐标也就是说基于模型本身参考系的UV坐标进行转换成我们需要的基于摄像机的UV坐标,所以我们第一个结构体可以这么写

struct a2v{
        float4 vertex:POSITION;
        float2 uv:TEXCOORD0;
};

写完了结构体,我们可以在vertex中直接调用这个结构体作为输入

v2f vert(a2v v):SV_POSITION{
    v2f o;
    o.vertex=UnityObjectToClipPos(v.vertex);
    o.uv=v.uv;
    return o;
}

当我们进行带有结构体的顶点着色器的编辑的时候,由于输出的并不一定是同一类型的数据,所以我们不会在函数上进行输出定义,我们输出的更多是一个带有多种属性的参数,一般我们就用o来作为顶点着色器的输出参数。所以这个顶点着色器的输入是参数v,v的数据从渲染pipline的顶点数据取得。v2f o;中的v2f 也是CG中的语句,意思是vertex to fragment 将顶点着色器中的数据传输到片元着色器中,相当于一个参数定义,将输出的参数暂时先储存在o下面。而o.uv=v.uv就是字面意义的坐标传输。

接着我们来定义fragment的结构体,我们从顶点着色器拿到了什么?变换后的顶点坐标与uv坐标,那我们要输出什么,顶点坐标不用变换,但我们需要输出的是一张我们准备好的自定义贴图而不是单纯的uv坐标,所以我们要用我们准备好的_MainTex匹配所得到的uv坐标,通俗的讲就是用贴图,点对点的进行每一个像素的颜色填充。所以我们首先定义结构体

struct v2f{
    float4 pos:SV_POSITION;
    float2 uv:TEXCOORD1;
};

我们将裁剪后的顶点数据给到pos,第二套纹理坐标给到uv作为fragment的输入。之后我们片元着色器的输出依旧是fixed4颜色,但这里我们将用tex2D函数进行对坐标的填充。

fixed4 frag(v2f i):SV_Target{
    return tex2D(_MainTex,i.uv)
}

结合上文,我们的完整代码应该是如下

Shader"Custom/textureShader"{
	Properties{
		_MainTex("Main Tex",2D) = "white"{}
		_Color("COlor Tint",Color) = (1,1,1,1)
	}
		Subshader{
			Pass{
				CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag

				fixed4 _Color;
				sampler2D _MainTex;

				struct a2v {
					float4 vertex:POSITION;
					float4 uv:TEXCOORD0;
				};

				struct v2f {
					float4 pos:SV_POSITION;
					float2 uv:TEXCOORD1;
				};

				v2f vert(a2v v):SV_POSITION {
					v2f o;
					o.pos = UnityObjectToClipPos(v.vertex);
					o.uv = v.uv;
					return o;
				}

				fixed4 frag(v2f i) :SV_Target{
					return tex2D(_MainTex,i.uv);
				}

				ENDCG
			}

		}
			Fallback "Diffuse"
}

但别急得激动,因为这个代码会报错,他给的报错信息应该是

invalid output semantic 'SV_POSITION': Legal indices are in [0,0]

错误是在SV_POSITION这个语义上,我们再次理解下这个语义,SV_POSITION是裁剪之后的空间顶点坐标,这个信息我们应该存储在了struct v2f这个结构体的pos之中,而这个结构体已经在frag作为输入中输出了这个顶点间坐标,而v2f vert(a2v v)所输出的也并不是仅仅的空间坐标,在这其实把SV_POSITION删除是一个正确的选项。所以修改之后的代码是

Shader"Custom/textureShader"{
	Properties{
		_MainTex("Main Tex",2D) = "white"{}
		_Color("COlor Tint",Color) = (1,1,1,1)
	}
		Subshader{
			Pass{
				CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag

				fixed4 _Color;
				sampler2D _MainTex;

				struct a2v {
					float4 vertex:POSITION;
					float4 uv:TEXCOORD0;
				};

				struct v2f {
					float4 pos:SV_POSITION;
					float2 uv:TEXCOORD1;
				};

				v2f vert(a2v v){
					v2f o;
					o.pos = UnityObjectToClipPos(v.vertex);
					o.uv = v.uv;
					return o;
				}

				fixed4 frag(v2f i) :SV_Target{
					return tex2D(_MainTex,i.uv);
				}

				ENDCG
			}

		}
			Fallback "Diffuse"
}
正确输出的贴图纹理

至此,我们在unity shader的编写结束。

我们接着在shader graph中实现我们的贴图效果

shader graph中的2D纹理贴图

需要注意的是如果我们直接创建texture节点是不能直接拖入Albedo的输入的,在shader graph中需要像上面的shader代码一样,进行一个纹理值的采样才能正常输入albedo。所以我们要将texture节点作为输入连接sampler2D这个节点,然后再将sampler2D这个节点的输出作为albedo的输入。

然后是我们再UE4中的材质shader编写,在UE4的shader编辑中,他的texture sample节点会总动将纹理进行采样,所以我们只需要进行将texture sample的rgb节点连接到basecolor就能将纹理输出

UE4中的纹理贴图

SD中进行纹理贴图与UE4中相似,但贴图节点的名称为bitmap。

SD中的bitmap节点

bitmap主要存储的是一个rgba的像素组,但SD作为一个造轮子的软件,他的材质不需要对geometry负责,所以一般不会在SD中进行使用贴图,也有其他的情况需要进行赋予贴图,这里不再赘述。