《Unity Shader入门精要》学习--高级篇--超级有用的噪声

·本专栏文章记录笔者阅读学习《Unity Shader入门精要》的感想与笔记,方便日后复习与查找



一、什么是噪声

噪声主要是指杂乱无章的效果。在这里我们主要通过噪声纹理的形式去应用这个噪声。

噪声纹理是一种程序纹理,它主要有两种:

  • Perlin噪声:用于生成更自然的噪声纹理
  • Worley噪声:用于模拟诸如石头、水、智障等多孔噪声。

在这一节接下来实现的效果中,我们主要应用的是Perlin噪声纹理


二、消融效果

消融效果是指让物体像被燃烧或者溶解了一样的效果

消融效果

这里我们主要通过噪声纹理+透明度测试的方式来实现这种透洞的效果(采样得到噪声颜色的r通道去进行透明度测试)

溶解的边缘的颜色我们通过对裁剪系数来插值得到

1.属性添加与CG定义

Properties{
		//普通计算光照相关
		_Color ("Color", Color) = (1, 1, 1, 1)
		_MainTex ("Main Tex", 2D) = "white" {}
		_BumpTex ("Bump Tex", 2D) = "bump" {}
		
		//噪声消融相关
		_NoiseTex ("Noise Tex", 2D) = "black" {}
		_NoiseAmount ("Noise Amount", Range(0, 1)) = 0.5
		_LineWidth ("Line Width", Range(0.0, 2.0)) = 0.1
		_BurnFirstColor ("Burn First Color", Color) = (1, 0, 0, 1)
		_BurnSecondColor ("Burn Second Color", Color) = (1, 0, 0, 1)
}

			float4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			sampler2D _BumpTex;
			float4 _BumpTex_ST;

			sampler2D _NoiseTex;
			float4 _NoiseTex_ST;
			float _NoiseAmount;
			float _LineWidth;
			float4 _BurnFirstColor;
			float4 _BurnSecondColor;

①主要分为正常计算光照部分(主颜色、主纹理、法线纹理),和消融相关部分属性(噪声纹理、噪声程度、消融边缘线的宽度、溶解边缘线的两种颜色)

2.顶点着色器中

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

struct v2f{
	float4 pos : SV_POSITION;
	float2 uvMainTex : TEXCOORD0;
	float2 uvBumpTex : TEXCOORD1;
	float2 uvNoiseTex : TEXCOORD2;
	float3 lightDir : TEXCOORD3;
	float3 worldPos: TEXCOORD4;
	SHADOW_COORDS(5)
}; 

v2f vert(a2v v){
	v2f o;

	o.pos = UnityObjectToClipPos(v.vertex);
	o.uvMainTex = TRANSFORM_TEX(v.texcoord, _MainTex);
	o.uvBumpTex = TRANSFORM_TEX(v.texcoord, _BumpTex);
	o.uvNoiseTex = TRANSFORM_TEX(v.texcoord, _NoiseTex);

	TANGENT_SPACE_ROTATION;	//准备在切线空间下计算关照
	o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;		//rotation是把模型空间转换到切线空间的变换矩阵
	o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

	TRANSFER_SHADOW(o);

	return o;
}

①在顶点着色器中,我们按常规对顶点位置、纹理坐标位置等进行变换

②我们打算在切线空间中去计算光照,所以把光照方向也顺便变换到切线空间中。(注意TANGENT_SPACE_ROTATION这个宏是定义在UnityCG.cginc当中

3.片元着色器中

fixed4 frag(v2f i) : SV_Target{
	//在这里完成对噪声纹理的采样
	fixed3 noise = tex2D(_NoiseTex, i.uvNoiseTex);

	//然后进行透明度测试
	clip(noise.r - _NoiseAmount);

	//之后对剩余部分正常进行光照计算(在切线空间中)
	fixed3 tangentNormal = UnpackNormal(tex2D(_BumpTex, i.uvBumpTex));
	fixed3 tangentLightDir = normalize(i.lightDir);
	fixed4 albode = tex2D(_MainTex, i.uvMainTex) * _Color;

	fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albode;
	fixed3 diffuse = _LightColor0.rgb * albode * saturate(dot(tangentNormal, tangentLightDir));

	//给消融制作一点细节
	fixed t = 1 - smoothstep(0.0, _LineWidth, noise.r - _NoiseAmount);	//混合系数--显然当裁剪系数noise.r - _NoiseAmount越小的时候,smoothstep得到的结果约趋近于0,t的系数也就越大
	fixed3 burnColor = lerp(_BurnFirstColor, _BurnSecondColor, t);	//t的系数越大,那就约接近第二种颜色
	burnColor = pow(burnColor, 5);

	UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
	fixed3 finalColor = lerp(ambient + diffuse * atten, burnColor, t * step(0.0001, _NoiseAmount));	//最后用t去给最终的颜色进行插值

	return fixed4(finalColor, 1.0);
}

①这里首先最关键的部分是对噪声纹理进行采样后,和噪声程度属性相减得到消融系数(noise.r-_Amount)以用于进行透明度测试。

②因为有法线纹理,我们这里把需要用于计算的向量都变换到切线空间当中去进行光照的计算。

③对于消融边缘线颜色和宽度的计算,首先计算出消融边缘颜色的系数t(它是当消融系数越小的时候,t就越大),当t越大的时候,最后用它插值得到的颜色就越接近于消融边缘颜色。而对于消融的两种颜色,也是通过t去混合,并通过幂函数去增大混合得到的颜色的对比度(亮的更亮,暗的更暗)

4.动态消融辅助脚本

好啦,现在我们只要动态地改变属性值_NoiseAmount,就可以让这个消融变得动态了起来。我们在一个脚本中让这个值循环地进行变化

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DissolveHelper : MonoBehaviour
{
    [Range(0f, 1f)]
    public float dissolveSpeed = 0.02f;

    private Material material;
    private Renderer renderer;

    private float dissovleAmount = 0.0f;

    private void Start()
    {
        renderer = GetComponent<Renderer>();
        material = renderer.material;
        material.SetFloat("_NoiseAmount", 0.0f);
    }

    private void Update()
    {
        if(material != null) {
            //让dissovleAmount一直在[0,1]之间循环
            dissovleAmount =  Mathf.Repeat(Time.time * dissolveSpeed, 1.0f);
            material.SetFloat("_NoiseAmount", dissovleAmount);
        }
    }
}

①这里主要用了一个Mathf.Repeat(Time.time * dissolveSpeed, 1.0f);让消融程度变量在[0,1]之间循环

总的来说就是:顶点着色器进行顶点向量变换、片元着色器中先采样噪声纹理、得到消融系数去裁剪、在切线空间中正常进行光照计算、用消融系数去计算消融边缘线系数t、用t去混合两种颜色并用幂函数去增大对比度、把光照颜色和最终的颜色再用t去混合、最后在脚本中动态地控制_NoiseAmount的属性去控制消融系数以让消融效果动态进行

5.关于消融的正确阴影投射

这里要消融的时候得到正确的阴影投射,需要我们也自己提供一个ShadowCaster类型的Pass才行

//这个Pass是为了让消融的投影一致
Pass{
	Tags { "LightMode" = "ShadowCaster"}

	CGPROGRAM

	#pragma vertex vert
	#pragma fragment frag

	#include "UnityCG.cginc"

	#pragma multi_compile_shadowCaster

	sampler2D _NoiseTex;
	float4 _NoiseTex_ST;
	float _NoiseAmount;

	struct v2f{
		V2F_SHADOW_CASTER;
		float2 uvNoiseTex : TEXCOORD1;
	};

	v2f vert(appdata_base v){
		v2f o;

		TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)	//顶点着色器中进行法线偏移

		o.uvNoiseTex = TRANSFORM_TEX(v.texcoord, _NoiseTex);

		return o;
	};

	fixed4 frag(v2f i) : SV_Target {
		fixed3 noise = tex2D(_NoiseTex, i.uvNoiseTex).rgb;

		clip(noise.r - _NoiseAmount);

		SHADOW_CASTER_FRAGMENT(i);		//顶点着色器中进行最后的投影计算
	}

	ENDCG
}

①这里自制的投影Pass需要v2f中包含V2F_SHADOW_CASTER,需要顶点着色器中TRANSFER_SHADOW_CASTER_NORMALOFFSET(o),需要片元着色器中SHADOW_CASTER_FRAGMENT(i)。这样子一直调用宏来完成投影的数值相关计算

②而其他的关于消融的计算和之前基本一样的,也是计算消融系数并用于透明组测试。(这里不用光照计算了的)

6.最终效果查看

动态消融的效果

看起来就像被烧毁了一样的


三、水波效果

折射比反射更强
反射比折射更强

之前我们使用过GrabPass(用于折射计算)立方体纹理(用于反射计算)搭配来制作过一个玻璃的效果。之前我们用的是一个静态的法线纹理来对玻璃内折射进行扰动。这里我们会通过噪声纹理来动态地对折射进行扰动,以得到类似上面两个图的水波效果。

同时我们这里用到的菲涅尔系数的计算公式是:fresnel = pow(1-max(0, v\cdot n), 4)

从这个公式中我们可以看出来。

  • v,n的夹角约小的时候(越垂直去看),v·n就越大,菲涅尔系数就越小,折射就越强,反射就越弱
  • v,n的夹角约大的时候(越水平去看),v·n就越小,菲涅尔系数就越大,折射就越弱,反射就越强

以下是具体实现的方法

1.属性添加与CG中定义

Properties
{
    _Color ("Main Color", Color) = (0, 0.15, 0.115, 1)      //控制水面颜色
    _MainTex ("Texture", 2D) = "white" {}       //水的主要纹理贴图
    _WaveMap ("Wave Map", 2D) = "bump" {}       //水的噪声纹理贴图(用于折射计算)
    _CubeMap ("Enviroment Cubemap", Cube) = "_Skybox" {}    //水的反射纹理贴图(用于反射计算)
    _WaveXSpeed ("Wave Horizontal Speed", Range(-0.1, 0.1)) = 0.01  //法线在横向移动速度
    _WaveYSpeed ("Wave Vertical Speed", Range(-0.1, 0.1)) = 0.01    //发现在纵向移动速度
    _Distortion ("Distortion", Range(0, 10000)) = 10      //扭曲度
}
  fixed4 _Color;
  sampler2D _MainTex;
  float4 _MainTex_ST;
  sampler2D _WaveMap;
  float4 _WaveMap_ST;
  samplerCUBE _CubeMap;
  fixed _WaveXSpeed;
  fixed _WaveYSpeed;
  fixed _Distortion;
  sampler2D _RefractionTex;   //声明折射纹理变量,让上一个Pass抓取的屏幕纹理存放到这里来
  float4 _RefractionTex_TexelSize;

①注意这个CG定义是在第二个Pass中第一个Pass是GrabPass,并把它抓取获得的纹理保存到_RefractionTex当中,然后因为要逐纹素地对他进行偏移,所以还需要_RefractionTex_TexelSize;这个变量的定义

2.SubShader的Tags与GrabPass

 SubShader
 {
     Tags {"Queue" = "Transparent" "RenderType"="Opaque" }

     //第一个Pass先抓取当前已经渲染完所有不透明物体后的图像
     GrabPass{"_RefractionTex"}  //存在折射纹理这个名字中

     //...

 }

①这里的渲染路径要改成Transparent,因为我们希望在所有不透明物体都渲染完毕之后再去抓取屏幕纹理,以在之后获得正确的折射效果。

②RenderType设置为Opaque是为了在需要的时候能够被正确渲染,标记它按不透明物体的设置进行渲染【比如打开深度写入,并且包含到深度纹理当中】(这一般发生在要获取摄像机的深度和法线纹理的时候)

3.顶点着色器中

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

struct v2f
{
    float4 pos : SV_POSITION;
    float4 scrPos : TEXCOORD9;
    float4 uv : TEXCOORD1;
    float4 TtoW0 : TEXCOORD2;
    float4 TtoW1 : TEXCOORD3;
    float4 TtoW2 : TEXCOORD4;
};

v2f vert (appdata v)
{
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
    o.uv.zw = TRANSFORM_TEX(v.texcoord, _WaveMap);

    o.scrPos = ComputeGrabScreenPos(o.pos); //由裁剪空间中顶点位置来计算屏幕上顶点位置

    float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    float3 worldNormal = normalize(UnityObjectToWorldNormal(v.normal));
    float3 worldTangent = normalize(UnityObjectToWorldDir(v.tangent.xyz));
    float3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

    o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
    o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
    o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
    
    return o;
}

①顶点着色器中主要用于把顶点位置、坐标纹理和向量进行变换。这里我们希望在世界空间中进行光照计算,所以我们需要计算保存出切线空间变换到世界空间的矩阵。

②这里我们对抓取的屏幕纹理进行采样需要用到屏幕位置,所以用到ComputeGrabScreenPos(o.pos)来计算,传入的的参数是顶点在裁剪空间中的坐标值注意这个计算得到的结果的范围是[-w,w]

4.片元着色器中

fixed4 frag (v2f i) : SV_Target
{
    float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
    fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));   //计算视角方向
    float2 speed = _Time.y * float2(_WaveXSpeed, _WaveYSpeed);      //计算移动速度

    //获取当前时刻这个像素位置的法线
    fixed3 bump1 = UnpackNormal(tex2D(_WaveMap, i.uv.zw + speed)).rgb;
    fixed3 bump2 = UnpackNormal(tex2D(_WaveMap, i.uv.zw - speed)).rgb;
    fixed3 bump = normalize(bump1 + bump2);

    //在切线空间中计算折射的偏移值
    float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;    //切线空间中沿着法线xy方向的偏移度
    i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;    //把偏移值和屏幕上点的深度值再相乘得到越深,偏移值越大的效果。最后偏移作用在屏幕坐标的xy方向上
    fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.w).rgb; //这里除以w的是因为:scrPos.xy的范围是[-w,w],而采样需要把它映射到[-1,1]当中(归一化设备坐标)

    //把这个法线转换到世界坐标下
    fixed3 worldNormal = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
    fixed4 texColor = tex2D(_MainTex, i.uv.xy + speed); //根据速度去偏移采样的水的纹理位置
    fixed3 reflectDir = reflect(-viewDir, worldNormal); //用光路的可逆性去获取这个射入当前摄像机中光线的来源
    fixed3 reflCol = texCUBE(_CubeMap, reflectDir).rbg * texColor.rgb * _Color.rgb;   //采样得到立方体反射颜色、主纹理颜色、和自定义属性颜色进行混合

    //计算菲涅尔系数
    fixed fresnel = pow(1 - saturate(dot(viewDir, worldNormal)), 4);     //(1 - (视角方向 点乘 法线方向)^4)
    fixed3 finalColor = reflCol * fresnel + refrCol * (1-fresnel);  //菲涅系数越大,折射越弱;菲涅尔系数越小,折射越强


    return fixed4(finalColor, 1.0);
}

①首先我们获取世界空间当中对需要的世界坐标位置、视角方向和水波移动速度

②接着我们对法线纹理的采样坐标根据速度进行正负两个方向的偏移,并把这两个方向偏移采样得到的法线值相加起来。

③接着我们在切线空间中计算偏移值:沿着②中计算得到的法线值的xy方向偏移_Distortion * _RefractionTex_TexelSize.xy个单位,并且通过z值来得到深度值越深、偏移值越大的效果。然后用这个偏移值对屏幕坐标进行偏移,然后用这个屏幕坐标进行采样【注意:这里屏幕坐标的范围是[-w,w],但是采样需要是[-1,1],所以我们采样的时候需要把它先做一个透视除法以映射到[-1,1]当中】,得到最后的折射颜色值。

④然后我们把法线值变换到世界坐标中,用它和视角方向的反方向得到光线方向,并用于在立方体纹理上进行采样得到反射的颜色值。

⑤最后计算菲涅尔系数,并用菲涅尔系数来混合折射颜色和反射颜色以求出最后的颜色值

总之就是:

顶点着色器中

  • 先计算得到顶点在屏幕坐标
  • 再计算切线空间到世界空间的变换矩阵

片元着色器中

  • 先对法线(噪声)纹理进行采样(采样的坐标需要根据速度进行正负两个方向的偏移)、把采样解码后得到的两个向量值相加并归一化得到最终的法线方向
  • 然后在切线空间中根据这个法线的xy方向去计算偏移值
  • (需要的话还可以根据屏幕坐标的深度值去进一步偏移)然后用这个偏移值去对屏幕坐标的xy方向进行偏移
  • 并让他进行透视除法得到最后的采样坐标以在抓取的屏幕纹理上进行采样得到折射颜色值
  • 接着再把这个向量变换到世界坐标中用于计算反射颜色值
  • 最后计算菲涅尔系数并把反射和折射颜色相混合

5.运行查看最终的效果

反射更强的时候
折射更强的时候

如果要是感觉波动程度不是很高,那就把_Distortion这个值拉高即可。


四、噪声加持的全局雾效

之前我们在《Unity Shader入门精要》学习--高级篇--深度和法线纹理:“我来助你”学习了如何用屏幕后处理效果制作基于高度的雾:

普通的基于高度的全局雾效

大致方法是:

①计算射线值

②通过射线值来快速计算当前像素在世界空间下位置

③通过世界空间下位置来计算雾的强度值

④根据雾的强度值来当前采样屏幕纹理得到的颜色值和雾的颜色值进行混合

但是这样制作出来的雾在同一高度上的厚度都是均匀静态的,在这里我们可以通过噪声纹理,在第③步计算雾的强度值的时候根据当前时刻采样得到的噪声值进行偏移以让雾变得不再均匀并动态起来。

加入噪声的全局雾效

1.雾的屏幕后处理脚本

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public class FogWithNoise : PostEffectBase
{
    public Shader fogWithDepthTexShader;
    private Material fogWithDepthTexMat;
    public Material material
    {
        get {
            fogWithDepthTexMat = CheckShaderAndCreatMaterial(fogWithDepthTexShader, fogWithDepthTexMat);
            return fogWithDepthTexMat;
        }
    }

    //生成深度纹理的摄像机
    private Camera myCam;
    //摄像机的位置
    private Transform camTrans;

    [Range(0.0f, 3.0f)]
    public float fogDensity = 1.0f;     //雾的强度
    public Color fogColor = Color.white;    //雾的颜色
    public float fogStart = 0.0f;       //雾开始的位置
    public float fogEnd = 2.0f;         //雾结束的位置

    public Texture noiseTexture;    //噪声纹理
    public float fogXSpeed = 0.1f;  //X方向的移动速度
    public float fogYSpeed = 0.1f;  //Y方向的移动速度
    public float noiseAmount = 1.0f;    //噪声采样的程度

    private void OnEnable()
    {
        myCam = GetComponent<Camera>();
        camTrans = myCam.transform;
        myCam.depthTextureMode |= DepthTextureMode.Depth;   //需要获取一个深度纹理
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (material == null) {
            Graphics.Blit(source, destination);
            return;
        }

        //用来记忆四个顶点到摄像机的向量
        Matrix4x4 frustumCorners = Matrix4x4.identity;

        float fov = myCam.fieldOfView;
        float near = myCam.nearClipPlane;
        float aspect = myCam.aspect;    //横纵比

        float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
        Vector3 toRight = camTrans.right * halfHeight * aspect;     //halfHeight * aspect就是半宽度的
        Vector3 toTop = camTrans.up * halfHeight;

        Vector3 topLeft = camTrans.forward * near + toTop - toRight;

        //计算得到射线大小系数
        float scale = topLeft.magnitude / near;

        //计算摄像机到近裁剪面四个角的射线
        topLeft.Normalize();
        topLeft *= scale;

        Vector3 topRight = camTrans.forward * near + toRight + toTop;
        topRight.Normalize();
        topRight *= scale;

        Vector3 bottomLeft = camTrans.forward * near - toTop - topRight;
        bottomLeft.Normalize();
        bottomLeft *= scale;

        Vector3 bottomRight = camTrans.forward * near + toRight - toTop;
        bottomRight.Normalize();
        bottomRight *= scale;


        //设置到矩阵的行中
        frustumCorners.SetRow(0, bottomLeft);
        frustumCorners.SetRow(1, bottomRight);
        frustumCorners.SetRow(2, topRight);
        frustumCorners.SetRow(3, topLeft);

        //把相关属性设置到材质中
        material.SetMatrix("_FrustumCornersRay", frustumCorners);

        material.SetFloat("_FogDensity", fogDensity);
        material.SetColor("_FogColor", fogColor);
        material.SetFloat("_FogStart", fogStart);
        material.SetFloat("_FogEnd", fogEnd);

        //动态雾相关参数
        material.SetTexture("_NoiseTex", noiseTexture);
        material.SetFloat("_FogXSpeed", fogXSpeed);
        material.SetFloat("_FogYSpeed", fogYSpeed);
        material.SetFloat("_NoiseAmount", noiseAmount);

        Graphics.Blit(source, destination, material);
    }
}

①这里计算近裁剪平面四个顶点的射线用的方法和之前是一样的

②这里主要新加入的纹理和属性是:噪声纹理、雾气XY方向的移动速度、噪声的程度;其他的属性和之前一样的

2.属性添加与CG定义

Properties
{
    _MainTex ("Texture", 2D) = "white" {}       //主纹理
    _FogDensity("Fog Density", Float) = 1.0     //雾气的强度 
    _FogColor("Fog Color", Color) =  (1,1,1,1)  //雾的颜色
    _FogStart("Fog Start", Float) = 0.0         //雾开始的高度
    _FogEnd("Fog End", Float) = 1.0             //雾结束的高度

    _NoiseTex ("Noise Texture", 2D) = "white"{}     //噪声纹理
    _FogXSpeed ("Fog Horizontal", Float) = 0.1      //纹理X方向的速度
    _FogYSpeed ("Fog Vertical Speed", Float) = 0.1  //纹理Y方向的速度
    _NoiseAmount ("Noise Amount", Float) = 1        //噪声的程度
}
 float4x4 _FrustumCornersRay;

 sampler2D _MainTex;
 half4 _MainTex_TexelSize;
 sampler2D _CameraDepthTexture;      //摄像机深度纹理
 half _FogDensity;
 half4 _FogColor;
 half _FogStart;
 half _FogEnd;

 sampler2D _NoiseTex;
 float4 _NoiseTex_ST;
 half _FogXSpeed;
 half _FogYSpeed;
 half _NoiseAmount;

①这里的属性主要也是添加了噪声纹理噪声采样坐标的XY的速度、以及噪声的程度值

②记得定义中也是要获取摄像机的深度纹理的【通过sampler2D _CameraDepthTexture

3.顶点着色器中

struct v2f{
    float4 pos : SV_POSITION;
    half2 uv : TEXCOORD0;
    half2 uv_depth : TEXCOORD1;
    float4 interpolateRay : TEXCOORD2;      //插值后获取的摄像机到像素的射线
};

v2f vert (appdata_img v){
    v2f o;

    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv = v.texcoord;      
    o.uv_depth = v.texcoord;

    //纹理坐标的原点如果在左上角,就需要进行y翻转的操作
    #if UNITY_UV_STARTS_AT_TOP
    if(_MainTex_TexelSize.y < 0)
        o.uv_depth.y = 1 - o.uv_depth.y;
    #endif

    //然后根据当前uv坐标的位置,确定index对应四个射线的哪一个
    int index = 0;
    if(o.uv_depth.x < 0.5 && o.uv_depth.y < 0.5){
        //纹理坐标在左下角范围
        index = 0;
    }
    else if(o.uv_depth.x > 0.5 && o.uv_depth.y < 0.5){
        //纹理坐标在右下角
        index = 1;
    }
    else if(o.uv_depth.x > 0.5 && o.uv_depth.y > 0.5){
        //纹理坐标在右上角
        index = 2;
    }
    else{
        //在左上角
        index = 3;
    }

    o.interpolateRay = _FrustumCornersRay[index];

    return o;
}

①这里通过当前顶点的纹理坐标所在的位置在确定基于哪一个射线来进行线性插值。

②注意这里因为是使用了深度纹理,有可能因为平台差异而导致纹理的坐标原点不同(DirectX的就是在左上角),不同的时候纹素值就会是负数,这个时候让纹理坐标的y 值用1-y来翻转过去即可。

③下面直接通过可能进行翻转过后的深度采样纹理坐标进行射线的选择。如果用的是原来的顶点坐标纹理进行选择,那最后还要判断一次是否需要翻转,需要的话就要index = 3-index

4.片元着色器中

fixed4 frag(v2f i) : SV_Target{
    float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));  //把深度纹理采样后得到的值变换到线性空间中(视角空间)
    float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolateRay.xyz;        //然后再根据射线信息和摄像机位置信息快速变换得到像素在世界空间中的位置

    float2 speed = _Time.y * float2(_FogXSpeed, _FogYSpeed);    //先计算得到速度(X,Y方向速度结合)
    float noise = (tex2D(_NoiseTex, i.uv + speed).r - 0.5) * _NoiseAmount;      //速度值用于直接加上uv值(-0.5是为了把他映射到[-0.5, 0.5]之间,就可以在零均值附近进行正负扰动了

    float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
    fogDensity = saturate(fogDensity * _FogDensity * (1 + noise));      //用噪声控制该处雾的强度 (1 + noise)将 [-0.5, 0.5]的噪声转换为 [0.5, 1.5]的强度系数 || 实现的效果:​​以基础雾效为中心进行对称增强/减弱​
    
    fixed4 finalColor = tex2D(_MainTex, i.uv);
    finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);

    return finalColor;
}

①这里通过射线快速地通过深度值来得到像素在世界空间下的坐标值

②然后根据速度值去偏移噪声纹理的采样坐标。注意这里采样后的值-0.5是为了把采样值由[0,1]映射到[-0.5,0.5]当中,这样就是正负均匀地进行采样的

③然后用用线性雾公式来计算雾的强度值,并把强度值和采样得到的(噪声值+1)进行相乘。(+1是为了强化噪声值)

④然后把最终的强度值用于在主纹理颜色和雾的颜色之间进行插值以得到最终的颜色

总的来说就是:

顶点着色器中

  • 根据顶点的纹理坐标选择对应的射线

片元着色器中

  • 根据射线和采样得到的深度值计算出像素的世界坐标
  • 然后采样根据速度偏移纹理坐标来对噪声值进行采样
  • 再根据世界坐标计算得到雾强度值
  • 再把雾强度值和噪声值进行运算得到最终强度值
  • 最后把强度值作为插值系数在主纹理颜色和雾的颜色之间进行插值运算

5.运行查看最后的效果


五、其他小知识汇总

①TANGENT_SPACE_ROTATION这个宏需要在顶点着色器的输入中包含【切线】
②能自动产生投影的回调是VertexLit,调用Specular也行,因为它里面也回调了VertexLit
③V2F_SHADOW_CASTER;及其之后的投影相关的变换和着色都需要引用头文件“UnityCG.cginc”
④Render是Mesh Render的基类(也是所有其他渲染器的积累:蒙皮渲染器等),MeshRender仅仅作用于静态网格
⑤pow(burnColor, 5)起到了​​强化视觉效果

  • 幂运算(Power)会使颜色值呈现非线性变化:
  • 暗色区域变得更暗(值向0压缩)
  • 亮色区域变得更亮(值向1扩展)

⑥UNITY_UV_START_AT_TOP是表示当前平台UV原点在左上角的宏。
而如果确实在左上角的话(一般Unity会帮助我们翻转它,就不会在左上角了),那它的纹素值_MainTex_TexelSize.y 会 < 0,说明当前的渲染目标没有被自动翻转。那我们就需要进行手动翻转了
⑦Shader中对某个数值进行加减乘除操作,有一部分是为了把他映射到一定的范围里以制造特殊的效果
⑧目前这个项目中用的都是Perlin噪声,还有一种是Worley噪声


六、总结

①本章中我们学习了如何利用噪声去实现一些特殊的动态效果。噪声是一种程序纹理。

②对于消融效果的实现,我们主要是通过对噪声纹理采样得到的值和_NoiseAmount相减得到消融系数以实现消融的效果。_NoiseAmount越大,消融的程度越大

③消融效果的边缘线的计算也是通过消融系数计算得到一个t系数以混合两种边缘颜色,并把混合得到的颜色和光照计算的颜色也用t进行混合操作

④step(0.0001, _NoiseAmount) 这个主要是为了保证_NoiseAmount为0的时候这个函数值一定为0,其他时候都为1。

⑤水波效果主要是用GrabPass计算折射、立方体纹理计算反射,然后折射计算的时候的法线纹理是在噪声纹理中采样解码得到的,并用于偏移像素的屏幕坐标值以得到折射的颜色值。反射就普通地计算反射方向光线就好啦。

⑥菲涅尔系数越大,折射越弱;菲涅尔系数越小,折射越强

⑦对于全局雾,噪声可以用来让同一高度的雾的强度值也有所偏移变换以使得雾不是那么均匀

⑧注意对一些特殊的动态效果,它的投影往往需要我们自定义一个投影Pass去实现正确的投影效果

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

萘柰奈

谢谢老板喵

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值