Resumen de técnicas de textura comunes para el renderizado en tiempo real: mapeo de paralaje

[Columna USparkle] Si tiene grandes habilidades, le encanta "investigar un poco", está dispuesto a compartir y aprender de los demás, esperamos que se una, deje que las chispas de la sabiduría choquen y se entrelacen, y permita que la transmisión del conocimiento sea ¡sin fin!

I. Resumen

Parallax Mapping (Mapeo de paralaje) es una tecnología de textura similar a los mapas normales. Pueden mejorar significativamente los detalles de la superficie del modelo/textura y darle una sensación de irregularidad, pero la irregularidad que trae el mapa normal no cambiará con el ángulo de visión. El cambio no se bloqueará entre sí. Por ejemplo, si observa una pared de ladrillos real, en un ángulo de visión más perpendicular a la pared, no podrá ver los espacios entre los ladrillos y el mapa normal de la pared de ladrillos nunca mostrará este tipo de oclusión. , porque solo se cambian las normales para afectar el resultado de la iluminación directa.

Efecto incorrecto utilizando solo un mapa normal sin una relación de oclusión correcta

Por lo tanto, es mejor dejar que la protuberancia afecte la posición de cada píxel en la superficie; podemos lograr este requisito a través del mapa de altura.

ejemplo

La forma más fácil es usar una gran cantidad de vértices y luego compensar las coordenadas de posición del vértice de acuerdo con el valor de altura muestreado de la imagen de arriba: mapeo de desplazamiento (Mapeo de visualización) , puede obtener el efecto de la imagen de la izquierda en la imagen de abajo (la densidad de vértices es 100*100). Sin embargo, tal número de vértices no es aceptable para los juegos de renderizado en tiempo real (o no vale la pena optimizarlos), y si el número de vértices es demasiado pequeño, habrá un fenómeno de bloque muy desigual, como se muestra en la imagen de la derecha. abajo (la densidad de vértices es 10*10). Entonces, a algunas personas inteligentes se les ocurrió la capacidad de compensar las coordenadas de textura de vértice: mapeo de paralaje (Mapeo de paralaje) , de modo que podamos usar un Quad para crear el efecto real de la imagen de la izquierda en la imagen de abajo, primero coloque el código fuente .

Comparación del efecto de la tecnología de mapeo de desplazamiento bajo diferentes densidades de vértices

2. Principio

Entonces, ¿cómo compensar las coordenadas de la textura para hacer la protuberancia? Debemos comenzar con lo que hemos observado:

Suponiendo que realmente tenemos una superficie tan rugosa y desigual (como la que se obtiene mediante un desplazamiento de vértice denso), entonces, cuando miramos la superficie en una determinada línea de visión en dirección V, lo que deberíamos ver es el punto B (es decir, la línea de intersección del mapa de vista y altura ). Pero como dijimos antes, estamos usando un Quad, por lo que lo que realmente vemos debería ser el punto A. La función del mapeo de paralaje es compensar las coordenadas de textura en A con las coordenadas de textura en B, de modo que incluso si el punto que vemos es A, el resultado del muestreo está en B, simulando así la diferencia de altura, entonces lo que tenemos que resolver es cómo Obtener las coordenadas de textura en B en A.

principio

Observe cuidadosamente la imagen de arriba, de hecho, A y B están en la línea recta donde se encuentra la dirección de la línea de visión V, por lo que nuestra dirección de desplazamiento es la dirección de la línea de visión normalizada, y el desplazamiento es el resultado H(A) del mapa de altura de muestreo en A, por lo que el vector de compensación es P ¯ en la figura, y necesitamos compensar a lo largo del plano donde se encuentra la coordenada de textura (UV), por lo que la compensación es la proyección de P ¯ en el plano, entonces lo que realmente vemos en el punto A está en la figura H(P), lo que significa que lo que obtenemos es en realidad un resultado similar al punto B.

Debido a que necesitamos compensar a lo largo del plano donde se ubican las coordenadas de la textura (UV), es necesario elegir el espacio tangente (es decir, girar la dirección de la vista hacia el espacio tangente y luego compensar las coordenadas de la textura), de modo que no No tiene que preocuparse de que el modelo tenga alguna. El desplazamiento al rotar ya no está a lo largo del plano UV. Consulte el mapa normal para el principio , por lo que se recomienda encarecidamente que primero comprenda el mapa normal al principio.

El resultado de compensación que Padj obtuvo para la coordenada de textura P de cualquier punto, la dirección normalizada de la línea de visión V y el resultado de muestreo del mapa de altura h:

Dividir por el componente Z es para la normalización: porque el componente Z es mayor cuando la línea de visión es más perpendicular al plano. Pero cuando la línea de visión es casi paralela al plano, el componente Z es muy pequeño. Dividirlo por el componente Z hará que el desplazamiento sea demasiado grande. Preste atención a la brecha en la imagen a continuación (usando el ejemplo del mapa de altura original ).

Cuanto mayor sea el desplazamiento cuando la línea de visión esté más cerca de la paralela al plano

Para mejorar este problema, podemos limitar el desplazamiento para que nunca sea mayor que la altura real (el desplazamiento debe ser el vector indicado por la línea de flecha gris en la figura a continuación, pero después del límite es el vector indicado por la línea de flecha negra). La ecuación es Padj=P+h*Vxy (es decir, no se divide por la componente Z y la velocidad de cálculo es más rápida).

Se puede comparar con el resultado de la compensación anterior demasiado grande

Pero debido a que el componente XY de la dirección de visualización seguirá siendo mayor a medida que la dirección de visualización sea más paralela al plano, el desplazamiento seguirá siendo mayor.

Hay otro problema: en la mayoría de los casos, nuestro enfoque anterior puede obtener buenos resultados, pero los resultados pueden no ser satisfactorios cuando la altitud cambia rápidamente: el resultado obtenido H(P) es muy diferente del punto B (punto azul) lejano.

Tres, date cuenta

1. Mapeo de paralaje
Podemos hacer un intento simple basado en los principios básicos mencionados en la Parte anterior. Aquí seguiremos usando el mapa normal, porque también dije en el artículo que resume el mapa normal que el mapa normal a menudo se basa en la altura . La textura se calcula, pero el mapa normal afecta a la normal, y los detalles de relieve se expresan a través de la iluminación, mientras que el mapeo de paralaje utiliza las coordenadas de textura desplazadas para obtener los resultados de muestreo de otras posiciones para expresar la altura, por lo que la combinación de la dos es como una combinación de dos espadas, el poder aumentó considerablemente.

float2 ParallaxMapping(float2 uv, half3 viewDir)
{
    float height = tex2D(_HeightMap, uv).r * _HeightScale;
    float2 offset = 0;
#if _OFFSETLIMIT //为了对比是否限制偏移量的效果
    offset = viewDir.xy;
#else 
    offset = viewDir.xy / viewDir.z;
#endif
    float2 p = offset * height; 
    return uv - p;
}

half3 viewDirWS = normalize(UnityWorldSpaceViewDir(positionWS));
float2 uv = i.uv.xy;
#ifdef _PARALLAXMAPPING
    half3 viewDirTS = normalize(mul(viewDirWS, float3x3(i.T2W0.xyz, i.T2W1.xyz, i.T2W2.xyz)));
    uv = ParallaxMapping(uv, viewDirTS);
#endif
//然后用偏移后的纹理坐标采样各种贴图即可

La imagen de la izquierda muestra el efecto de usar solo el mapa normal, y la imagen de la derecha muestra el efecto de agregar el mapeo de paralaje

Puede ver que después de compensar las coordenadas de la textura puede haber un problema con la posición del borde, porque el borde puede estar fuera del rango de 0 a 1 después de compensar, para Quad, simplemente puede descartar la parte que está fuera de rango, pero para otros complejos Simplemente descartar el modelo puede no resolver el problema.

if (uv.x > 1 || uv.y > 1 || uv.x < 0 || uv.y < 0)
    discard;

Mucho más limpio después de desechar

En el código fuente de Shader de Unity, también nos proporciona la función de mapeo de paralaje:

// Calculates UV offset for parallax bump mapping
inline float2 ParallaxOffset( half h, half height, half3 viewDir )
{
    h = h * height - height/2.0;
    float3 v = normalize(viewDir);
    v.z += 0.42;
    return h * (v.xy / v.z);
}

Aunque el efecto actual es lo suficientemente bueno, los dos problemas planteados al final de la Parte anterior aún existen. La Sección 6.8.1 de la cuarta edición de Real Time Rendering proporciona muchos materiales de referencia para resolver estos problemas, que resumimos a continuación. más común de ellos, si quieres entender más profundamente, te recomiendo leer el texto original.

2. Mapeo de paralaje pronunciado
La causa raíz de los dos problemas mencionados al final de la Parte anterior es que el desplazamiento es demasiado grande, por lo que podemos seguir el ejemplo de Ray Marching y utilizar el enfoque de aproximación gradual para encontrar el desplazamiento adecuado. Pero esto inevitablemente requerirá muestreo múltiple, y el consumo de rendimiento será mayor.El uso inicial de esta idea es el mapeo de paralaje pronunciado (Steep Parallax Mapping).

Como se muestra en la figura a continuación, divida el rango de profundidad (0 (posición del plano) -> 1 (profundidad máxima de muestreo)) en múltiples capas con la misma profundidad h (la profundidad de la capa inferior h = 0.2), y encuentre el desplazamiento de textura correspondiente a la profundidad de capa h Cambie huv, y luego atraviese cada capa de arriba a abajo: use huv para compensar las coordenadas de textura y muestree el mapa de altura.Si el valor de profundidad de la capa actual es menor que el valor muestreado, continuamos con bajar hasta la profundidad de la capa actual Los resultados del muestreo son más grandes que el mapa de altura, lo que significa que encontramos la primera capa debajo de la superficie (es decir, donde se considera detectada la intersección de la línea de visión y el mapa de altura, aunque sea aproximada ).

T representa el número de recorridos, el punto morado es el valor de profundidad de la capa actual y el punto azul claro es el valor de profundidad muestreado

float2 ParallaxMapping(float2 uv, float3 viewDir)
{
    // 优化:根据视角来决定分层数(因为视线方向越垂直于平面,纹理偏移量较少,不需要过多的层数来维持精度)
    float layerNum = lerp(_MaxLayerNum, _MinLayerNum, abs(dot(float3(0,0,1), viewDir)));//层数
    float layerDepth = 1 / layerNum;//层深
    float2 deltaTexCoords = 0;//层深对应偏移量
#if _OFFSETLIMIT //建议使用偏移量限制,否则视线方向越平行于平面偏移量过大,分层明显
    deltaTexCoords = viewDir.xy / layerNum * _HeightScale;
#else
    deltaTexCoords = viewDir.xy / viewDir.z / layerNum * _HeightScale;
#endif
    float2 currentTexCoords = uv;//当前层纹理坐标
    float currentDepthMapValue = tex2D(_HeightMap, currentTexCoords).w;//当前纹理坐标采样结果
    float currentLayerDepth = 0;//当前层深度
    // unable to unroll loop, loop does not appear to terminate in a timely manner
    // 上面这个错误是在循环内使用tex2D导致的,需要加上unroll来限制循环次数或者改用tex2Dlod
    // [unroll(100)]
    while(currentLayerDepth < currentDepthMapValue)
    {
        currentTexCoords -= deltaTexCoords;
        // currentDepthMapValue = tex2D(_HeightMap, currentTexCoords).r;
        currentDepthMapValue = tex2Dlod(_HeightMap, float4(currentTexCoords, 0, 0)).r;
        currentLayerDepth += layerDepth;
    }
    return currentTexCoords;
}

Ahora el efecto es casi realista:

La imagen de la derecha es un mapa de paralaje pronunciado, el número mínimo actual de capas es 10 y el número máximo de capas es 30

Pero cuando lo miramos desde un ángulo más paralelo a la superficie, aunque el número de capas aumenta con el ángulo de visión, todavía hay un claro fenómeno de estratificación:

La forma más sencilla es seguir aumentando el número de capas, pero esto afectará en gran medida al rendimiento (de hecho, ya es grave). Hay métodos destinados a solucionar esto: en lugar de la primera capa debajo de la superficie, hágalo entre las capas de profundidad antes y después de la intersección (entre la última capa sobre la superficie y la primera capa debajo de la superficie) Interpolar para encontrar una mejor intersección coincidente ubicaciones _ Las dos soluciones más populares se denominan Relief Parallax Mapping y Parallax Occlusion Mapping. Relief Parallax Mapping es más preciso, pero tiene más sobrecarga de rendimiento que Parallax Occlusion Mapping. Echemos un vistazo a estos dos planes.

3. Mapeo de oclusión de paralaje
La distancia entre el valor de muestreo del mapa de altura de la capa de profundidad antes y después de la intersección y el valor de profundidad de las dos capas se usa como el peso de la interpolación lineal, y luego las coordenadas de textura correspondientes a las dos capas antes y después de la intersección se interpolan linealmente. H(T3) y H(T2) en la siguiente figura son dos triángulos similares con líneas azules, líneas moradas y líneas amarillas respectivamente. La longitud de la línea azul es la distancia entre el valor de muestreo del mapa de altura y la profundidad de la capa correspondiente, de modo que podamos usar Triángulos similares para obtener la relación entre las líneas moradas, que puede corresponder directamente al resultado del desplazamiento de coordenadas de la textura (es decir, el desplazamiento correspondiente a Tp, por lo que está más cerca del punto de intersección).

// 陡峭视差映射的代码
//......
// get texture coordinates before collision (reverse operations)
float2 prevTexCoords = currentTexCoords + deltaTexCoords;
// get depth after and before collision for linear interpolation
float afterDepth  = currentDepthMapValue - currentLayerDepth;
float beforeDepth = tex2D(_HeightMap, prevTexCoords).r - currentLayerDepth + layerDepth;
// interpolation of texture coordinates
float weight = afterDepth / (afterDepth - beforeDepth);
float2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);

return finalTexCoords;  

Perfecto, sin capas.

4. Mapeo de paralaje de relieve
Antes de hablar sobre el mapeo de paralaje de relieve, echemos un vistazo al mapeo de relieve: en lugar de capas como el mapeo de paralaje pronunciado, encontramos el mejor valor entre el rango de profundidad (0-> 1) por dicotomía:

dicotomía

Como se muestra en la figura, tomamos el punto medio 1 de AB, reemplazamos B con 1, luego tomamos el punto medio 2 entre 1 y A, reemplazamos A con 2, y luego tomamos el punto medio 3 entre 1 y 2, que es lo que queremos La intersección de la línea de visión y el mapa de altura, este es el flujo de la dicotomía. Pero en algunos casos, pueden surgir problemas:

La línea de visión y los mapas de altura pueden tener múltiples intersecciones

En la dirección de la línea de visión en la imagen, obtendremos 3 usando el método de dicotomía, pero de hecho 3 ha sido bloqueado, y lo que obtendremos debería ser el punto azul de arriba. En este momento, podemos usar el resultado del mapeo de paralaje pronunciado: como se muestra en la figura a continuación, primero busque la primera capa (3) debajo de la superficie a través del mapeo de paralaje pronunciado y luego realice una búsqueda binaria con A, por lo que se llama mapeo de paralaje de relieve.

Pero aún se puede optimizar, porque el mapa de paralaje pronunciado ya puede obtener la capa de profundidad antes y después de la intersección (la última capa sobre la superficie y la primera capa debajo de la superficie, como 2 y 3 en la figura anterior), luego directamente aquí La búsqueda binaria entre dos capas de profundidad es suficiente: es suficiente para comprender a través del código, de hecho, está más subdividido, por lo que es más preciso que el mapeo de oclusión de paralaje. Todavía hay una ligera delaminación, pero en su mayor parte es invisible. Y debido a que la diferencia de profundidad entre dos capas adyacentes es la profundidad de la capa, no es necesario calcular la última posición más alta que la superficie como el mapeo de oclusión de paralaje, pero obviamente este último no necesita subdividirse sino interpolarse, por lo que el rendimiento es mejor. .

// 陡峭视差映射的代码
//......
// 二分查找
float2 halfDeltaTexCoords = deltaTexCoords / 2;
float halfLayerDepth = layerDepth / 2;
currentTexCoords += halfDeltaTexCoords;
currentLayerDepth += halfLayerDepth;

int numSearches = 5; // 5次基本上就最好了,再多也看不出来了
for(int i = 0; i < numSearches; i++)
{
halfDeltaTexCoords = halfDeltaTexCoords / 2;
halfLayerDepth = halfLayerDepth / 2;
currentDepthMapValue = tex2D(_HeightMap, currentTexCoords).r;
if(currentDepthMapValue > currentLayerDepth)
{
currentTexCoords -= halfDeltaTexCoords;
currentLayerDepth += halfLayerDepth;
}
else
{
currentTexCoords += halfDeltaTexCoords;
currentLayerDepth -= halfLayerDepth;
}
}

return currentTexCoords;

5. Agregar sombras
es la mejor manera de mostrar la oclusión, y también es muy necesario. Actualmente, la pared de ladrillos que usamos tiene una pequeña profundidad de compensación, por lo que las sombras sin autooclusión se ven bien, pero el efecto después de agregar sombras será uniforme. más impresionante (y, por supuesto, más adecuado para profundidades de desplazamiento más grandes):

Para hacer la sombra más obvia, aumenté la escala de altura/profundidad y la fuerza de la sombra.

La idea de hacer sombras es más simple. Podemos usar los resultados del mapeo de oclusión de paralaje para encontrar el punto de intersección hacia arriba. Si lo hay, significa que está ocluido, y la intensidad de la sombra se puede determinar de acuerdo con el número. de puntos de intersección, porque cuanto más profundo, más fácil Si la sombra está bloqueada, cuanto mayor sea el número de puntos de intersección, más fuerte será la sombra, de modo que se puede hacer una sombra con una transición suave entre la luz y la oscuridad.

// 输入的initialUV和initialHeight均为视差遮挡映射的结果
float ParallaxShadow(float3 lightDir, float2 initialUV, float initialHeight)
{
float shadowMultiplier = 1; //默认没有阴影
if(dot(float3(0, 0, 1), lightDir) > 0) //Lambert
{
                //根据光线方向决定层数(道理和视线方向一样)
float numLayers = lerp(_MaxLayerNum, _MinLayerNum, abs(dot(float3(0, 0, 1), lightDir)));
float layerHeight = 1 / numLayers; //层深
float2 texStep = 0; //层深对应偏移量
        #if _OFFSETLIMIT
        texStep = _HeightScale * lightDir.xy / numLayers;
        #else
                texStep = _HeightScale * lightDir.xy / lightDir.z / numLayers;
        #endif
                // 继续向上找是否还有相交点
float currentLayerHeight = initialHeight - layerHeight; //当前相交点前的最后层深
float2 currentTexCoords = initialUV + texStep;
float heightFromTexture = tex2D(_HeightMap, currentTexCoords).r;
                float numSamplesUnderSurface = 0; //统计被遮挡的层数
while(currentLayerHeight > 0) //直到达到表面
{
if(heightFromTexture <= currentLayerHeight) //采样结果小于当前层深则有交点
numSamplesUnderSurface += 1; 

currentLayerHeight -= layerHeight;
currentTexCoords += texStep;
heightFromTexture = tex2Dlod(_HeightMap, float4(currentTexCoords, 0, 0)).r;
}
shadowMultiplier = 1 - numSamplesUnderSurface / numLayers; //根据被遮挡的层数来决定阴影强度
}
return shadowMultiplier;
}

Pero ahora la sombra es dura y tiene un efecto de capas.

La práctica de la sombra suave: la optimización está en los comentarios, que se pueden comparar con el código anterior. ¡El punto no es determinar la intensidad de la sombra en función del número de capas que se cruzan!

// 输入的initialUV和initialHeight均为视差遮挡映射的结果
float ParallaxShadow(float3 lightDir, float2 initialUV, float initialHeight)
{
    float shadowMultiplier = 0;
    if (dot(float3(0, 0, 1), lightDir) > 0) //只算正对阳光的面
    {
        // 根据光线方向决定层数(道理和视线方向一样)
float numLayers = lerp(_MaxLayerNum, _MinLayerNum, abs(dot(float3(0, 0, 1), lightDir)));
float layerHeight = initialHeight / numLayers; //从当前点开始计算层深(没必要以整个范围)
        float2 texStep = 0; //层深对应偏移量
    #if _OFFSETLIMIT
texStep = _HeightScale * lightDir.xy / numLayers;
    #else
        texStep = _HeightScale * lightDir.xy / lightDir.z / numLayers;
    #endif
        // 继续向上找是否有相交点
float currentLayerHeight = initialHeight - layerHeight; //当前相交点前的最后层深
float2 currentTexCoords = initialUV + texStep;
float heightFromTexture = tex2D(_HeightMap, currentTexCoords).r;
int stepIndex = 1; //向上查找次数
        float numSamplesUnderSurface = 0; //统计被遮挡的层数
while(currentLayerHeight > 0) //直到达到表面
{
    if(heightFromTexture < currentLayerHeight) //采样结果小于当前层深则有交点
            {
numSamplesUnderSurface += 1;              
                float atten = (1 - stepIndex / numLayers); //阴影的衰减值:越接近顶部(或者说浅处),阴影强度越小
                // 以当前层深到高度贴图采样值的距离作为阴影的强度并乘以阴影的衰减值
float newShadowMultiplier = (currentLayerHeight - heightFromTexture) * atten;
shadowMultiplier = max(shadowMultiplier, newShadowMultiplier);
    }

    stepIndex += 1;
    currentLayerHeight -= layerHeight;
    currentTexCoords += texStep;
    heightFromTexture = tex2Dlod(_HeightMap, float4(currentTexCoords, 0, 0)).r;
}

if(numSamplesUnderSurface < 1) //没有交点,则不在阴影区域
    shadowMultiplier = 1;
else 
    shadowMultiplier = 1 - shadowMultiplier;
    }
    return shadowMultiplier;
}

sombras suaves perfectas

4. Método de producción

1. Valores de escala de grises de texturas procedimentales
Se pueden utilizar un gran número de técnicas de generación de texturas procedimentales (ruido, SDF, geometría computacional...)

2. Cálculo de la relación entre la luz y la sombra
El mapa de colores (Albedo/Difuso) que usamos a menudo contiene detalles ricos en luces y sombras, como Photo Shop > Filtro > 3D > Generar mapa de relieve (altura), una de las informaciones que se pueden utilizado es el incorporado La relación entre la luz y la sombra (por ejemplo, el espacio en la pared en la imagen de abajo es negro)

3. Dibujar + usar procesamiento de imágenes

4. Generar modelos de alta precisión.
Como dijimos anteriormente, una práctica común en el desarrollo de juegos es crear modelos de alta precisión en el software de modelado. Después de ajustar los efectos, se pueden simplificar en mallas de baja precisión e importarlos al motor. Los modelos de alta precisión en sí mismos son Se utiliza una gran cantidad de detalles de rendimiento de vértice, que se pueden hacer con herramientas de escultura, y la cantidad modificada se puede escribir en una textura después del revelado UV y exportar como un mapa de altura.

5. Aplicación

El mapeo de paralaje es una muy buena tecnología para mejorar los detalles de la escena, puedes buscar efectos increíbles, pero al usarlo, aún debes considerar que traerá un poco de falta de naturalidad, por lo que la mayoría de las veces el mapeo de paralaje se usa en el Superficies del suelo y de la pared, en estos casos, señalar el contorno de una superficie no es fácil, y la dirección de visualización tiende a ser perpendicular a la superficie. De esta manera, es difícil notar la falta de naturalidad del mapeo de disparidad.

1. Superficie de la pared: generada por PS

Mapa de color - Mapa normal - Mapeo de paralaje

2. Grietas del terreno: pintadas a mano.

Grietas del terreno basadas en el mapeo de paralaje

3. Simulación dinámica de nubes y niebla: uso de ruido

Nubes dinámicas y niebla usando mapeo de paralaje

4. Trayectorias en el terreno: generadas dinámicamente


Este es el artículo 1403 de Yuhu Technology, gracias al autor por contribuir. Bienvenido a volver a publicar y compartir, por favor no reimprimir sin la autorización del autor. Si tiene ideas o descubrimientos únicos, comuníquese con nosotros y discutan juntos.

Página de inicio del autor: https://www.zhihu.com/people/LuoXiaoC

Gracias nuevamente por compartir. Si tiene ideas o descubrimientos únicos, comuníquese con nosotros y analicemos juntos.

Supongo que te gusta

Origin blog.csdn.net/UWA4D/article/details/131228724
Recomendado
Clasificación