En el capítulo anterior, implementamos un material de flujo básico basado en compensación UV.
[Unity Shader] Conceptos básicos de la representación del agua: material de flujo de fluido basado en la distorsión de la textura.
Pero obviamente, cuando el cuerpo de agua necesita fluir de manera direccional, la solución anterior se vuelve menos realista.
1. Implementar compensación direccional UV básica
Simplemente tómese el tiempo directamente, escriba una función para compensar la dirección UV y utilice el mapa de muestreo UV compensado.
float2 DirectionalFlowUV(float2 uv, float2 flowVector, float tilling, float time)
{
//实例的flowVector为(0,1)
uv.y -= time;
return uv * tilling;
}
Si desea ajustar diferentes ángulos, ajuste el flowVector correspondiente.
float2 DirectionalFlowUV(float2 uv, float2 flowVector, float tilling, float time)
{
uv -= flowVector * time;
return uv * tilling;
}
1.1 Implementar rotación ultravioleta
Por supuesto, es solo un simple desplazamiento a lo largo de la dirección xy de la uv. No importa cómo combine diferentes vectores de flujo, solo puede lograr la traducción de la uv en diferentes direcciones. El efecto general de la uv permanece paralelo a la dirección x. .
Si queremos que los UV se compensen en su conjunto, será mejor que movamos la textura en Photoshop o SD,
será mejor que cambiemos el método de cálculo de los UV.
Para el punto de muestreo p en un mapa, ya sea que uv se cambie en las direcciones xey (para obtener p1, p2) o que XOR se mezcle con un cambio lineal (para obtener p3), el resultado final es simplemente lograr En lugar de una traducción lineal, todos los UV se traducirán según la misma lógica y, finalmente, se convertirán en la traducción de toda la textura.
Entonces, la lógica que debemos implementar es encontrar las coordenadas uv después de rotar en un ángulo específico con el origen como el centro del círculo mientras se mantiene la distancia al origen (es decir, el radio).
De esta manera, todas las coordenadas pueden rotar con diferentes desplazamientos según sus diferentes radios, logrando así la rotación del mapa general.
Para mostrar la situación general, aquí eliminamos la textura que bloquea la línea de visión, de modo que el punto p en sí tenga un cierto ángulo en lugar de estar ubicado directamente en el eje x.
Ahora necesitamos encontrar las coordenadas s, t del punto p' que gira b grados en el ángulo de rotación predeterminado de un grado.
Por supuesto, las longitudes de OP y OP' son las mismas, ambas r.
Aquí debes usar la fórmula de división de funciones trigonométricas de la escuela secundaria, la publicaré directamente.
对于s,t来说,其值为
s = r * cos(a + b)
t = r * sin(a + b)
对于x, y来说,其值为
x = r * cosa
y = r * sina
对s,t进行拆分
s = r * (cosa * cosb - sina * sinb)
t = r * (sina * cosb + cosa * sinb)
直接用x,y的值带入即可
s = x * cosb - y * sinb
t = y * cosb + x * sinb
Entonces resolvimos la relación correspondiente entre las coordenadas después de la rotación, las coordenadas antes de la rotación y los ángulos.
Cuando se gira 45 grados, podemos encontrar la relación correspondiente como
s = x * √2/2 - y * √2/2
t = x * √2/2 + y * √2/2
我们直接将其化归,就能够得到对应的float2x2矩阵
(1, 1)
(-1, 1)
Utilice la matriz correspondiente para multiplicar el uv original y obtener el valor uv después de girar 45 grados.
Tenga en cuenta que el parámetro de función trigonométrica predeterminado de Unity aquí no es un ángulo, sino un radian, que debe convertirse.
float2 DirectionalFlowUV(float2 uv, float angle, float tilling, float time)
{
float2 uv2;
uv2.x = uv.x * cos(angle) - uv.y * sin(angle);
uv2.y = uv.x * sin(angle) + uv.y * cos(angle);
uv2 += time;
return uv2 * tilling;
}
A continuación, debemos resolver el problema de las normales incorrectas después de la rotación.
Generamos directamente las normales como colores y podemos ver que las normales se muestran normalmente sin rotación.
Sin embargo, después de girar 90 grados, la dirección de la normal sigue siendo hacia el eje y (es decir, la parte azul en la imagen de arriba sigue siendo azul en la imagen de abajo), lo que significa que después de girar 90 grados, la normal es todavía mirando hacia el eje z. y la rotación no se completa.
Necesitamos usar la matriz rotada y también rotar los resultados muestreados.
float2 dh2;
dh2.x = dh.x * cos(_Rotate) - dh.y * sin(_Rotate);
dh2.y = dh.x * sin(_Rotate) + dh.y * cos(_Rotate);
float3 worldNormal = normalize(float3(-dh2.x, 1, -dh2.y));
1.2 Mapa de flujo de muestreo
Aunque es más divertido controlarlo directamente usando ángulos, para usar la dirección proporcionada por el mapa de flujo, todavía tenemos que modificar las partes correspondientes.
float2 DirectionalFlowUV(float2 uv,float3 flowVectorAndSpeed, float tilling, float time)
{
float2 uv2;
float2 dir = normalize(flowVectorAndSpeed.xy);
uv2.x = uv.x * dir.x - uv.y * dir.y;
uv2.y = uv.x * dir.y + uv.y * dir.x;
uv2 += time * flowVectorAndSpeed;
return uv2 * tilling;
}
Sin embargo, los resultados del muestreo directo son obviamente muy confusos y los ángulos de rotación de cada coordenada son inconsistentes. Debido a la introducción de cambios no lineales, el efecto es completamente inconsistente.
2. Implementar rotación + flujo en mosaico
En nuestro plan para lidiar con la distorsión UV traslacional, utilizamos principalmente dos UV con diferencias de tiempo para muestrear y luego mezclar los resultados del muestreo. Pero esto se debe a que la función frac en sí tiene un período de reinicio y los cambios de rotación no pueden resolver el problema de la distorsión excesiva simplemente mediante el procesamiento del tiempo.
Como era imposible manejar todo el avión de manera global, dividimos el avión en múltiples áreas, de modo que la dirección del flujo en cada área fuera la misma, y las áreas se mezclaron para lograr continuidad. Este método es el algoritmo Tiled Directional Flow propuesto por Frans van Hoesel.
2.1 Implementar muestreo en escalera
Agregue una función de paso basada en el parámetro de configuración _GridResolution para realizar el cambio uv correspondiente en pasos.
float2 tiledUV = floor(i.uv * _GridResolution) / _GridResolution;
float3 flow = tex2D(_FlowMap, tiledUV);
Dado que los UV en la misma escalera son consistentes, podemos obtener un plano dividido en cuadrados y los UV en un solo cuadrado cambian en una dirección.
2.2 Mezclar múltiples cuadrados
Primero, encapsulamos los cálculos de rotación y muestreo de escalera existentes en métodos que son independientes de fs.
float3 FlowCell(float2 uv, float time)
{
float2 uvTiled = floor(uv * _GridResolution) / _GridResolution;
float3 flow = tex2D(_FlowMap, uvTiled);
flow.xy = flow.xy * 2 -1;
float2 uvFlow = DirectionalFlowUV(uv, flow, _Tiling, time);
float3 dh = UnpackDerivativeHeight(tex2D(_MainTex, uvFlow));
float3 dh2;
float2 dir = normalize(flow.xy);
dh2.x = dh.x * dir.x - dh.y * dir.y;
dh2.y = dh.x * dir.y + dh.y * dir.x;
dh2.z = dh.z;
return dh2;
}
fixed4 frag (v2f i) : SV_Target
{
float flowTime = _Time.y;
float3 dh = FlowCell(i.uv, flowTime);
float3 worldNormal = normalize(float3(-dh.x, 1, -dh.y));
fixed3 col = dh.z * dh.z * _Color;
fixed3 diffuse = _LightColor0 * dot(_WorldSpaceLightPos0, worldNormal);
return fixed4(col.xyz + diffuse, 1.0);
}
La implementación del muestreo de compensación también es muy simple: agregue un desplazamiento de número de punto flotante bidimensional como parámetro y agréguelo durante el muestreo de pasos para recopilar información sobre los pasos adyacentes.
float3 FlowCell(float2 uv, float2 offset, float time)
{
float2 uvTiled = floor(uv * _GridResolution + offset) / _GridResolution;
//..........
}
Hacemos directamente un muestreo doble y luego nos referimos a mezclar.
//in fragment shader
float3 dhA = FlowCell(i.uv, float2(0, 0), flowTime);
float3 dhB = FlowCell(i.uv, float2(1, 0), flowTime);
float3 dh = 0.5 * dhA + 0.5 * dhB;
Se puede ver que en la dirección u(x), el problema de la discontinuidad obviamente ha mejorado algo.
Para garantizar el efecto de mezcla, utilizamos un método de mezcla de peso dinámico, utilizando la función decimal frac para obtener los cambios de UV en una sola celda y asignamos el peso de mezcla de acuerdo con los diferentes valores de u.
float t = frac(i.uv.x * _GridResolution);
float wA = 1 - t;
float wB = t;
float3 dh = wA * dhA + wB * dhB;
Pero ahora puedes ver que todavía hay espacios en el medio de cada celda, lo cual es causado por el salto de t.
Por un lado, comprimimos el desplazamiento para que una sola celda pueda desplazarse dos veces.
float3 FlowCell(float2 uv, float2 offset, float time)
{
offset *= 0.5;
//.............
}
Por otro lado, para el cálculo de pesos también se debe duplicar la amplitud de cambio para igualar los dos saltos de cada cuadrado.
float t = abs(2 * frac(i.uv.x * _GridResolution) - 1);
// float t = frac(i.uv.x * _GridResolution);
float wA = 1 - t;
float wB = t;
A continuación hacemos un muestreo compensado en la dirección v.
Tenga en cuenta que aquí estamos mezclando muestras para cuatro bloques diferentes, en lugar de simplemente mezclar muestras para los bloques directamente adyacentes del bloque 0,0.
float3 dhA = FlowCell(i.uv, float2(0, 0), flowTime);
float3 dhB = FlowCell(i.uv, float2(1, 0), flowTime);
float3 dhC = FlowCell(i.uv, float2(0, 1), flowTime);
float3 dhD = FlowCell(i.uv, float2(1, 1), flowTime);
float2 t = abs(2 * frac(i.uv * _GridResolution) - 1);
// float t = frac(i.uv.x * _GridResolution);
float wA = (1 - t.x) * (1 - t.y);
float wB = t.x * (1 - t.y);
float wC = (1 - t.x) * t.y;
float wD = t.x * t.y;
float3 dh = wA * dhA + wB * dhB + wC * dhC + wD * dhD;
En este punto, se ha logrado la fusión por rotación UV, pero si miras de cerca, todavía hay una vaga sensación de cuadrícula, especialmente en la dirección horizontal.
Necesitamos mover el resultado del piso para que esté en el medio de cada celda.
float3 FlowCell(float2 uv, float2 offset, float time)
{
float shift = 1 - offset;
offset *= 0.5;
shift *= 0.5;
float2 uvTiled = (floor(uv * _GridResolution + offset) + shift) / _GridResolution;
//..........
}
Podemos ajustar el cultivo y _gridResolution para obtener diferentes efectos, pero tenga en cuenta que _gridResolution no se puede ajustar demasiado.
2.3 Complementar otros parámetros de control
Ahora agregue también los otros parámetros.
Ajustar dinámicamente la fuerza normal (altura de ola)
//in FlowCell function
dh2 *= flow.z * _HeightScaleModulated + _HeightScale;
Agregue ruido del mapa de flujo a la labranza
//in FlowCell function
float tilling = flow.z * _TilingModulated + _Tiling;
float2 uvFlow = DirectionalFlowUV(uv, flow, tilling, time);
Podemos agregar un parámetro que controle el escalado de uso. Cuando el rango de muestreo se controla dentro de un rango más pequeño del mapa de flujo, el flujo general mostrará un flujo relativamente direccional.
_UvTilingScale("uv tiling scale", Range(0.1, 1)) = 1
//in FlowCell function
float3 flow = tex2D(_FlowMap, uvTiled * _UvTilingScale);
Personalmente prefiero la sensación de _UvTilingScale más bajo.
Cuando _gridResolution es demasiado pequeño, aún se producirán defectos.
2.4 Implementar mezcla de baja _gridResolution
Entonces la pregunta es, el lado derecho está borroso, pero los resultados del cálculo en el lado izquierdo son más claros, ¿qué debemos hacer?
La respuesta también es muy simple: compensar el resultado de la izquierda y mezclarlo con el resultado de la derecha puede aliviar el problema hasta cierto punto.
En consecuencia, es agregar 1/4 de compensación al uv (antes ya estaba compensado a 1/2). Al mismo tiempo, separamos los cuatro métodos de mezcla de bloques.
float3 FlowCell(float2 uv, float2 offset, float time, bool gridOffsetTag)
{
float shift = 1 - offset;
offset *= 0.5;
shift *= 0.5;
if(gridOffsetTag)
{
offset += 0.25;
shift -= 0.25;
}
//........
}
float3 FlowGrid(float2 uv, float flowTime, bool gridOffsetTag)
{
float3 dhA = FlowCell(uv, float2(0, 0), flowTime, gridOffsetTag);
float3 dhB = FlowCell(uv, float2(1, 0), flowTime, gridOffsetTag);
float3 dhC = FlowCell(uv, float2(0, 1), flowTime, gridOffsetTag);
float3 dhD = FlowCell(uv, float2(1, 1), flowTime, gridOffsetTag);
float2 t = uv * _GridResolution;
if(gridOffsetTag)
{
t += 0.25;
}
t = abs(2 * frac(t) - 1);
// float t = frac(i.uv.x * _GridResolution);
float wA = (1 - t.x) * (1 - t.y);
float wB = t.x * (1 - t.y);
float wC = (1 - t.x) * t.y;
float wD = t.x * t.y;
float3 dh = wA * dhA + wB * dhB + wC * dhC + wD * dhD;
return dh;
}
//in fs
float3 dh = FlowGrid(i.uv, flowTime, false);
dh = (dh + FlowGrid(i.uv, flowTime, true)) * 0.5;
Antes de mezclar (izquierda) Después de mezclar (derecha)