分类目录归档:学习笔记

Rtr4读书笔记序言

开个坑,写一下RTR4的读书笔记。RTR4指的是《Real Time Rendering(4th Edition)》这本书,国内据我所知只有第二版的翻译,第三版的中文版应该会在近期上,所以第四版是纯英文的版本。这个读书笔记会结合他人的读书笔记外加自己的阅读理解,主要参考的读书笔记是毛星云大佬的《<Real Time Rendering 3rd>提炼总结》。如果有人能看到,希望大佬们斧正。

其中标星号的为相对于rtr第三版改动幅度比较大的章节,我之后会专门针对之前相对薄弱的章节进行梳理

RayMarching’s Post Work

在学习关于Ray Marching的过程中,我发现对于Ray Marching的理解可以通过ShaderToy来加深一下。

Ray Marching原理

如上图所属,我们的摄像机需要获取4组数据,UV、摄像机位置(CameraPos)、视线(ViewDir)、缩放,然后根据这四组数据进行输出光线,这个光线是个向量,然后经过一个黑箱操作(这里指Ray Marching)输出一组数据,我们在这组数据上进行材质的添加与系列操作,最终输出到我们的片元着色器/像素着色器(fragment shader/pixel shader)。而我们需要进行的操作就是如何进行黑箱计算。

Ray Marching中的黑箱

如上我们具体所做的就是上面图中的操作,不断的使用光线步进来检测物体

第一次步进

第一次步进接触到最靠近相机的物体表面,我们在这打个标记,表示第一次步进,然后依次为圆心再对其他的光线进行步进

第二次步进

第二次步进我们在次发散出去的光又一次接触到了离此处最近的物体表面也就是黄圈的半径,我们在此再次打个点进行我们下一步的步进。然后周而复始达到最远的点。

最终的步进

如此我们就能勾勒出一个大体的形状.我们在这一部的代码可以如下

float raymarching(vec3 ro, vec3 rd){
	float dO=0.;
        
        for(int i=0; i<MAX_STEPS;i++){
        	vec3 p = ro + rd*dO;
            float dS = GetDist(p);
            dO +=dS;
            if (dO>MAX_DIST || dS<SURF_DIST) break;
        
        
        }
        
    return dO;
}

在这个函数之前,我们需要再写一段函数来获得步进距离的函数。

获得步进距离

首先我们先获得两个距离,一个是相机到地面的距离,一个是相机到物体表面的距离,在这里物体是一个球,相机到地面的距离可以通过cameraPos.y-plane.y来获得,在这里相机到地面的距离就是相机的y坐标。相机到球体表面的距离是相机坐标减去球心坐标取模,再减去球的半径。我们为了确保光线能够安全的达到物体表面,我们需要将相机与任意物体的距离作为初始的步进距离,在这里我们用相机到地面的距离。

安全步进

所以此处的步进方式应该如下

    如此步进便能安全有效的达到物体的表面。此处代码为

float GetDist(vec3 p){
	vec4 s = vec4 (0, 1, 6, 1);
    
    float sphereDist = length(p-s.xyz)-s.w;
    float planeDist=p.y;
    
    float d = min(sphereDist,planeDist);
    return d;
    


}

之后添加灯光并获取法线就能进行简单的模型编写。

完整代码如下:

#define MAX_STEPS 100
#define MAX_DIST 100.
#define SURF_DIST .01


float GetDist(vec3 p){
	vec4 s = vec4 (0, 1, 6, 1);
    
    float sphereDist = length(p-s.xyz)-s.w;
    float planeDist=p.y;
    
    float d = min(sphereDist,planeDist);
    return d;
    


}


float raymarching(vec3 ro, vec3 rd){
	float dO=0.;
        
        for(int i=0; i<MAX_STEPS;i++){
        	vec3 p = ro + rd*dO;
            float dS = GetDist(p);
            dO +=dS;
            if (dO>MAX_DIST || dS<SURF_DIST) break;
        
        
        }
        
    return dO;



}

vec3 GetNormal(vec3 p){
	float d = GetDist(p);
    vec2 e = vec2(.01,0);
    
    vec3 n = d- vec3(
        GetDist(p-e.xyy),
        GetDist(p-e.yxy),
        GetDist(p-e.yyx));
        return normalize(n);
}

float GetLight(vec3 p){
    vec3 lightPos = vec3(0, 5, 6);
    lightPos.xy += vec2(sin(iTime),cos(iTime))*2.;
    vec3 l = normalize(lightPos-p);
    vec3 n = GetNormal(p);
    
    float dif = clamp(dot(n,l),0.,1.);
    
    float d =  raymarching(p+n*SURF_DIST*2.,l);
    if(d<length(lightPos-p)) dif *=.1;
    
    return dif;


}



void mainImage( out vec4 fragColor, in vec2 fragCoord)
{
    vec2 uv = (fragCoord-0.5*iResolution.xy)/iResolution.y;
    
    vec3 col = vec3(0);
    
    vec3 ro =vec3(0,1,0);
    vec3 rd = normalize(vec3(uv.x,uv.y,1));
    
	float d=raymarching(ro,rd);
    
    vec3 p = ro + rd * d;
    
    float dif = GetLight(p);
    col = vec3(dif);
    
   
    
    fragColor = vec4(col,1.0);



}

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那样有自由性。