【unity shader】水体渲染基础-水下透视效果

接下来是水体渲染基础的最后一篇,通过水面看到水下的物体,并呈现深度效果。

1. 搭建简单演示场景

我们直接搭一个小场景。
在这里插入图片描述
增加水面,赋予uv变形的水面材质,并增加透明度的设置。

在这里插入图片描述

SubShader
    {
    
    
        Tags {
    
     "RenderType"="Transparent" "Queue" = "Transparent" }
        LOD 100

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

            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            //.......返回的color结果,添加一个控制透明度的参数
         }
         //注意FallBack也要注释掉
    }

2. 基于雾效实现水深效果

水体会吸收光线,所以真实的水体并不是完全透明的。此外,水体对不同频率的光吸收率不同,蓝光被吸收最少。
故深度越深,水中的物体就会变成蓝色。

我们当然可以直接上一个全局雾,但这里我们最好还是使用仅面向水体的雾效计算。

这里开始,我们新增一个水下计算相关的cginc和一个返回水下片元颜色结果的函数。
新建cginc文件时我们需要注意,在windows文件夹下创建txt文件,并注意要修改文件后缀。

#include "LookingThroughWater.cginc"
float3 ColorBelowWater () 
{
    
    
	//目前只返回黑色
    return 0;
}

//返回值乘以ColorBelowWater()的结果,透明度调整为1

首先我们定义了最简单的水下片元颜色计算,得到了石油一般的流体
在这里插入图片描述
那么要计算深度雾,首先我们需要一个摄像机深度贴图。

// in xxx.cginc
sampler2D _CameraDepthTexture;

另外,在着色器计算时,我们需要获取对应的屏幕空间坐标。

在surface shader里面加入screenPos:

struct Input 
{
    
    
	float2 uv_MainTex;
	float4 screenPos;
};void surf (Input IN, inout SurfaceOutputStandard o) 
{
    
    
	…
	
	o.Albedo = ColorBelowWater(IN.screenPos);
	o.Alpha = 1;
}

在unlit shader里面,我们可以在片元着色器中增加VPOS语义,实现平面空间坐标的引入。

由于VPOS和SV_POSITION无法在同一个v2f结构中存在,所以我们必须删去原有的v2f中的SV_POSITION,并使之在顶点着色器的参数中通过out语义单独输出。

struct v2f
{
    
    
    float2 uv : TEXCOORD0;
    //......
    // float4 vertex : SV_POSITION;
};

v2f vert (appdata_tan v, out float4 vertex : SV_POSITION)
{
    
    
    v2f o;
    vertex = UnityObjectToClipPos(v.vertex);
    //.......
    
    return o;
}

fixed4 frag (v2f i, UNITY_VPOS_TYPE screenPos : VPOS) : SV_Target
{
    
    
    //....
}

同样的,我们也可以直接在进行计算,那样就无需使用VPOS,也不需要删去v2f结构体中的SV_POSITION定义。

v2f vert (appdata_tan v)
{
    
    
    v2f o;
    
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.screenpos = ComputeScreenPos(o.vertex);

    //.......
}

2.1 获取水下片元到水面的距离

逻辑不难,即是通过水底的片元深度-水面的片元深度,求解对应片元水体的厚度。

float3 ColorBelowWater (float4 screenPos) 
{
    
    
    float2 uv = screenPos.xy / screenPos.w;
    
    float backgroundDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv));
	float surfaceDepth = UNITY_Z_0_FAR_FROM_CLIPSPACE(screenPos.z);

    float depthDifference = backgroundDepth - surfaceDepth;
    
    //除以二十,拉开层次差别,这个20的常量,我们可以理解为最大深度
    //所有常量,最好都根据实际搭的场景深度,进行灵活调整
	return depthDifference/20;
}

注意这里片元着色器的返回值换成了纯ColorBelowWater() 的结果。
在这里插入图片描述
若此时我们得到了黑白颠倒的结果,则可能是深度贴图的v坐标是从上到下计算的。对这种情况,我们需要对v维度的uv进行取反。

//in xxxx.cginc
flaot4 _CameraDepthTexture_TexelSize;

float3 ColorBelowWater (float4 screenPos) 
{
    
    
    float2 uv = screenPos.xy / screenPos.w;
    #if UNITY_UV_STARTS_AT_TOP
		if (_CameraDepthTexture_TexelSize.y < 0) {
    
    
			uv.y = 1 - uv.y;
		}
	#endif
    
    //.........
}

2.2 获取水底渲染帧缓冲

解决了深度信息的计算,新的问题又来了,我们直接把深度信息乘算已有的结果的话,无法正确反映水下的颜色信息。

//in frag shader
return fixed4((col * _BaseColor + diffuse + specular)* ColorBelowWater(i.screenpos), _AlphaScale);

在这里插入图片描述
当然我们可以自作聪明地去调整alpha值来稀释黑色效果,但随之而来又会直接破坏深度效果。
在这里插入图片描述
所以我们需要将原本的水下渲染的颜色结果,和深度结算的结果进行差值混合。

因为原本的水体shader计算的只有水面的颜色结果,所以我们铁定是不可能在单个pass里面完成混合了。

我们单独增加一个GrabPass ,提前存储其他物体渲染的结果。由于透明物体渲染顺序本身就在非透明物体后,如果想面向非透明物体获取GrabPass,要注意渲染顺序问题。

根据unity文档的描述,grabpass只能抓取帧缓冲信息,拥有两种调用方法。在不提供目标贴图时,结果会被存储到_GrabTexture;而用户需要指定grabpass输出的暂存贴图时,需要通过双引号给出。
在这里插入图片描述

SubShader
    {
    
    
        Tags {
    
     "RenderType"="Transparent" "Queue" = "Transparent" }
        LOD 100
		
		//增加一个GrabPass,将背景物体渲染的结果预先存储到_WaterBackground中,供后续颜色插值混合使用
        GrabPass {
    
    "_WaterBackground"}

        Pass
        {
    
    
            //水体渲染pass
        }
    }

//in xxx.cginc

float3 ColorBelowWater (float4 screenPos) 
{
    
    
    float2 uv = screenPos.xy / screenPos.w;
    #if UNITY_UV_STARTS_AT_TOP
		if (_CameraDepthTexture_TexelSize.y < 0) {
    
    
			uv.y = 1 - uv.y;
		}
	#endif
    
    float3 backgroundColor = tex2D(_WaterBackground, uv).rgb;

    return backgroundColor;
}

可以看到在满alpha的情况下,也有了透视的效果。
在这里插入图片描述

2.3 完成背景渲染和水深的插值混合

在属性中新增两个雾效相关的参数:

_WaterFogColor ("Water Fog Color", Color) = (0, 0, 0, 0)
_WaterFogDensity ("Water Fog Density", Range(0, 2)) = 0.1

根据水底颜色(雾颜色),背景颜色进行差值混合,插值因子是雾浓度和深度的结合。

//in xxx.cginc
// update ColorBelowWater( )
float3 backgroundColor = tex2D(_WaterBackground, uv).rgb;
float fogFactor = exp2(-_WaterFogDensity * depthDifference);

return lerp(_WaterFogColor, backgroundColor, fogFactor);

在这里插入图片描述
现在,我们可以通过_WaterFogDensity 来控制水的散射效果,实现水体的深度差别。
在这里插入图片描述
然后对alphascale值做调整,显然现在我们不需要一个控制输出颜色透明度的参数,而是需要一个控制水体是否为透明深度渲染的参数。

原本的alphascale主要用于控制片元着色器颜色计算结果的透明度。
在这里插入图片描述
现在我们将其调整,让其影响fogFactor最终的计算结果,并使片元着色器的返回的w值固定为1

//in .cginc ColorBelowWater
return lerp(_WaterFogColor, backgroundColor, fogFactor * _AlphaScale);

与调整_WaterFogDensity 的效果不同,调节_AlphaScale主要影响是否进行深度混合效果的计算。
在这里插入图片描述

3.实现水下物体的扭曲

有生活经验的朋友们都知道,水下的物体在有水波时,会出现扭曲现象,参考下图中水下鱼类的边缘,随着水波出现了一定程度的扭曲。
在这里插入图片描述
实现的逻辑不复杂,即是让水下的部分的采样uv,沿水波的法线方向(x,z方向)做偏移即可。

//额外新增了参数_RefractionStrength,用于控制采样水下颜色结果的uv的偏移程度
float3 ColorBelowWater (float4 screenPos, float3 worldNormal) 
{
    
    
    float2 uvoffset = worldNormal.xz * _RefractionStrength;
    float2 uv = (screenPos.xy + uvoffset) / screenPos.w;
    //.........
}

水下扭曲现象从无到有的对比
在这里插入图片描述

3.1 修正水上物体的误偏移

看上面的代码就知道,由于uv偏移是全局性的计算,所以会导致很多明明没有位于水下的物体,其对应的水面也产生了扭曲颜色的情况。
在这里插入图片描述
修正的方法很简单,就是我们经判断后,只对水下的片元做uv偏移即可,对于水上的uv,则沿用原本的uv,并注意需要重算depthDifference。

float3 ColorBelowWater (float4 screenPos, float3 worldNormal) 
{
    
    
    //......新增一个originUV,用于存储偏移前的uv

    if(depthDifference < 0)
    {
    
    
        uv = originUV;
        #if UNITY_UV_STARTS_AT_TOP
		if (_CameraDepthTexture_TexelSize.y < 0) {
    
    
			uv.y = 1 - uv.y;
		}
	    #endif
	    //使用偏移前的uv采样颜色缓冲,会导致偏移后的uv采样的深度差,与颜色不匹配
	    //这里同时重采样backgroundDepth,再算一次depthDifference 
        backgroundDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv));
        depthDifference = backgroundDepth - surfaceDepth;
    }

    float3 backgroundColor = tex2D(_WaterBackground, uv).rgb;
    float fogFactor = exp2(-_WaterFogDensity * depthDifference);

    return lerp(_WaterFogColor, backgroundColor, fogFactor * _AlphaScale);
}

在这里插入图片描述

4.实现水面波动

调整好水下折射效果后,我们仍然觉得水面的效果明明如此的波涛汹涌,但是水平面仍然明镜止水般镇定,显然不太符合常识。
在这里插入图片描述
最后在前面的渲染基础上,我们在顶点着色器内对flowmap进行采样,求解其向量长度,并用于太高vertex的位置(y方向)。

当然,这里使用的是100x100的plane,此外,在顶点着色器无法使用tex2D进行贴图采样,我们需要使用tex2Dlod来采样lowmap,并提供四位浮点数的uv。

我们给uv的第三,第四位置为0。

在这里插入图片描述

v2f vert (appdata_tan v)
{
    
    
    v2f o;
    o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    float2 flowVec;
    flowVec = tex2Dlod(_FlowMap, float4(o.uv + _Time.y * _Speed, 0.0, 0.0)).rg;
    flowVec = flowVec * 2 -1;

    o.vertex = UnityObjectToClipPos(v.vertex + float3(0.0, length(flowVec) * _HeightScale, 0.0));
    o.screenpos = ComputeScreenPos(o.vertex);

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

    o.t2w_0 = float4(worldTangent.x,worldBiTangent.x,worldNormal.x, worldPos.x);
    o.t2w_1 = float4(worldTangent.y,worldBiTangent.y,worldNormal.y, worldPos.y);
    o.t2w_2 = float4(worldTangent.z,worldBiTangent.z,worldNormal.z, worldPos.z);
    
    return o;
}

这样水体与浸没水体的物体边缘的交互会随着时间产生一定波动,能够显得更加真实一些。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/misaka12807/article/details/132594033