【Sombreador de unidad】 Conceptos básicos de la representación del agua: efecto de perspectiva submarina

El siguiente es el último artículo sobre los conceptos básicos de la renderización del agua, cómo ver objetos submarinos a través de la superficie del agua y mostrar el efecto de profundidad.

1. Construya una escena de demostración sencilla.

Preparemos una pequeña escena.
Insertar descripción de la imagen aquí
Agregue superficie de agua, proporcione material de superficie de agua deformada por rayos UV y aumente la configuración de transparencia.

Insertar descripción de la imagen aquí

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

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

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

2. Realice el efecto de profundidad del agua basado en el efecto de niebla.

El agua absorbe la luz, por lo que el agua real no es completamente transparente. Además, los cuerpos de agua tienen diferentes tasas de absorción de luz en diferentes frecuencias, siendo la luz azul la que menos se absorbe.
Por lo tanto, cuanto más profunda sea la profundidad, los objetos en el agua se volverán azules.

Por supuesto, podemos aplicar directamente una niebla global, pero aquí es mejor utilizar el cálculo del efecto de niebla solo para cuerpos de agua.

Comenzando aquí, agregamos un cginc relacionado con los cálculos submarinos y una función que devuelve el resultado del color del fragmento submarino.
Al crear un nuevo archivo cginc, debemos prestar atención a la creación de un archivo txt en la carpeta de Windows y tener cuidado de modificar el sufijo del archivo.

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

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

Primero, definimos el cálculo más simple del color de los fragmentos submarinos y obtuvimos un fluido similar al petróleo.
Insertar descripción de la imagen aquí
Para calcular la niebla de profundidad, primero necesitamos un mapa de profundidad de la cámara.

// in xxx.cginc
sampler2D _CameraDepthTexture;

Además, durante el cálculo del sombreador, necesitamos obtener las coordenadas del espacio de pantalla correspondientes.

Agregue screenPos al sombreador de superficie:

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

En el sombreador apagado, podemos agregar semántica VPOS al sombreador de fragmentos para introducir coordenadas de espacio plano.

Dado que VPOS y SV_POSITION no pueden existir en la misma estructura v2f, debemos eliminar SV_POSITION en el v2f original y hacer que se genere por separado a través de la semántica en los parámetros del sombreador de vértices.

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
{
    
    
    //....
}

De manera similar, también podemos realizar cálculos directamente, por lo que no es necesario usar VPOS o eliminar la definición SV_POSITION en la estructura v2f.

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

    //.......
}

2.1 Obtener la distancia desde el fragmento submarino hasta la superficie del agua.

La lógica no es difícil, es decir, a través de la profundidad del fragmento en el fondo del agua: la profundidad del fragmento en la superficie del agua, se encuentra el espesor del fragmento correspondiente en el cuerpo de agua.

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;
}

Tenga en cuenta que el valor de retorno del sombreador de fragmentos aquí se reemplaza por el resultado de ColorBelowWater() puro.
Insertar descripción de la imagen aquí
Si obtenemos un resultado en blanco y negro invertido en este momento, es posible que la coordenada v del mapa de profundidad se calcule de arriba a abajo. En este caso, necesitamos negar uv en la dimensión v.

//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 Obtener el búfer del cuadro de renderizado submarino

Después de resolver el cálculo de la información de profundidad, surge un nuevo problema: si multiplicamos directamente la información de profundidad calculando los resultados existentes, no podrá reflejar correctamente la información de color bajo el agua.

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

Insertar descripción de la imagen aquí
Por supuesto, podemos ser inteligentes y ajustar el valor alfa para diluir el efecto negro, pero esto destruirá directamente el efecto de profundidad.
Insertar descripción de la imagen aquí
Por lo tanto, necesitamos mezclar de manera diferente los resultados originales del color de representación bajo el agua y los resultados del asentamiento en profundidad.

Debido a que el sombreador de agua original solo calcula el resultado del color de la superficie del agua, definitivamente nos resulta imposible completar la mezcla en una sola pasada.

Agregamos un GrabPass por separado para almacenar los resultados de renderizado de otros objetos por adelantado. Dado que el orden de representación de los objetos transparentes es posterior a los objetos no transparentes, si desea obtener GrabPass para objetos no transparentes, debe prestar atención al problema del orden de representación.

Según la descripción de la documentación de Unity , grabpass solo puede capturar información del frame buffer y tiene dos métodos de llamada. Cuando no se proporciona la textura de destino, el resultado se almacenará en _GrabTexture; cuando el usuario necesite especificar la salida de la textura temporal mediante grabpass, deberá indicarse entre comillas dobles.
Insertar descripción de la imagen aquí

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;
}

Puedes ver que cuando el alfa está lleno, también hay un efecto de perspectiva.
Insertar descripción de la imagen aquí

2.3 Representación completa del fondo y combinación de interpolación de profundidad del agua

Se agregan a las propiedades dos nuevos parámetros relacionados con el efecto de niebla:

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

Según el color del fondo del agua (color de la niebla), el color de fondo se mezcla diferencialmente y el factor de interpolación es una combinación de la concentración de la niebla y la profundidad.

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

return lerp(_WaterFogColor, backgroundColor, fogFactor);

Insertar descripción de la imagen aquí
Ahora podemos usar _WaterFogDensity para controlar el efecto de dispersión del agua para lograr diferencias de profundidad en el cuerpo de agua.
Insertar descripción de la imagen aquí
Luego ajuste el valor de la escala alfa. Obviamente ahora no necesitamos un parámetro para controlar la transparencia del color de salida, sino un parámetro para controlar si el cuerpo de agua se representa como una profundidad transparente.

La escala alfa original se utiliza principalmente para controlar la transparencia de los resultados del cálculo de color del sombreador de fragmentos.
Insertar descripción de la imagen aquí
Ahora lo ajustamos para que afecte el resultado del cálculo final de fogFactor y fijamos el valor w devuelto por el sombreador de fragmentos en 1.

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

A diferencia del efecto de ajustar _WaterFogDensity, ajustar _AlphaScale afecta principalmente si se calcula el efecto de fusión de profundidad.
Insertar descripción de la imagen aquí

3. Date cuenta de la distorsión de los objetos submarinos.

Todos los amigos con experiencia en la vida saben que los objetos bajo el agua se distorsionarán cuando haya olas en el agua. Consulte el borde del pez submarino en la imagen a continuación, que aparece hasta cierto punto con las olas en el agua.
Insertar descripción de la imagen aquí
La lógica de implementación no es complicada, es decir, el UV muestreado de la parte submarina se desplaza a lo largo de la dirección normal de la onda de agua (dirección x, z).

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

Comparación del fenómeno de distorsión submarina desde cero.
Insertar descripción de la imagen aquí

3.1 Corregir el falso desplazamiento de objetos en el agua.

Al observar el código anterior, puede ver que, dado que la compensación UV es un cálculo global, hará que muchos objetos que no están bajo el agua tengan sus correspondientes superficies de agua distorsionadas en color.
Insertar descripción de la imagen aquí
El método de corrección es muy simple, es decir, después del juicio, solo hacemos compensación uv para los fragmentos bajo el agua, para los uv sobre el agua, usamos el uv original y prestamos atención a la necesidad de volver a calcular la diferencia de profundidad.

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);
}

Insertar descripción de la imagen aquí

4. Date cuenta de las fluctuaciones de la superficie del agua.

Después de ajustar el efecto de refracción bajo el agua, todavía sentimos que el efecto de la superficie del agua es obviamente muy entrecortado, pero la superficie horizontal sigue tan tranquila como un espejo, lo que obviamente no es coherente con el sentido común.
Insertar descripción de la imagen aquí
Finalmente, basándonos en la representación anterior, tomamos una muestra del mapa de flujo en el sombreador de vértices, resolvemos la longitud del vector y lo usamos para la posición (dirección y) del vértice demasiado alto.

Por supuesto, aquí se usa un plano de 100x100. Además, tex2D no se puede usar para el muestreo de texturas en el sombreador de vértices. Necesitamos usar tex2Dlod para muestrear el mapa bajo y proporcionar uv de punto flotante de cuatro bits .

Damos la tercera y cuarta posiciones de uv 0.

Insertar descripción de la imagen aquí

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;
}

De esta manera, la interacción entre el cuerpo de agua y el borde del objeto sumergido en el cuerpo de agua fluctuará con el tiempo, haciéndolo parecer más realista.
Insertar descripción de la imagen aquí

Supongo que te gusta

Origin blog.csdn.net/misaka12807/article/details/132594033
Recomendado
Clasificación