Blending y Unity Terrain: cómo deshacerse de las intersecciones y dejar de hacer que te duelan los ojos

Para obtener un mundo realista dentro del juego, es necesario tener en cuenta la interacción de varios accidentes geográficos entre sí y con otros modelos. Y si las líneas de intersección visibles entre los modelos 3D estropean la integridad de la imagen, vale la pena considerar cómo eliminarlas. El caso más común de tales líneas, que pueden ser familiares para muchos, es la intersección de vallas publicitarias de partículas con geometría opaca.

imagen

Otro ejemplo es la inquietante composición natural de la intersección de rocas y vegetación con la superficie del paisaje en escenas "al aire libre".

imagen

Además de varios métodos de suavizado (SSAA, MSAA, CSAA, FXAA, NFAA, CMAA, DLAA, TAA, etc.), que aunque mitigan la apariencia desafiante de tales líneas de intersección, pero no corrigen completamente la situación, existen técnicas más efectivas. Los consideraremos.

Mezcla de profundidad


Unity tiene una solución integrada para eliminar las intersecciones visibles entre partículas transparentes y geometría opaca llamadas partículas blandas. Los sombreadores que admiten este efecto mejoran aún más la transparencia de las partículas, dependiendo de cuán pequeña sea la diferencia entre la profundidad del fragmento de partícula y la profundidad de la geometría opaca.

imagen
El principio de funcionamiento de las partículas blandas

Obviamente, para el correcto funcionamiento de las partículas blandas, se requiere un tampón de profundidad. En el caso de sombreado diferido, el búfer de profundidad se forma en la etapa de renderizado de búferes de pantalla completa, y teniendo en cuenta el MRT (objetivos de renderizado múltiple, no la tomografía de resonancia magnética), su presencia no se expresa en costos computacionales adicionales.

En el caso del sombreado hacia adelante y el uso de la tubería de Unity Legacy, se requería un pase adicional para representar la geometría opaca en el búfer de profundidad [1] . Este pase se activa asignando el valor apropiado a la propiedad Camera.depthTextureMode. Esta propiedad no está disponible en la ventana del inspector, pero está disponible en la API [2] .

Ahora puede implementar su propia versión de Scriptable Render Pipeline con sombreado hacia adelante, que con la ayuda de MRT puede representar simultáneamente tanto el búfer de profundidad como el búfer de color.


Eliminación de líneas de intersección en sombreadores que admiten partículas blandas

En general, no existen obstáculos técnicos para utilizar el método de combinación de profundidad para eliminar intersecciones visibles de modelos 3D con el paisaje:

Ver código
// Blending with depth buffer

#include "UnityCG.cginc"

float BlendStart;
float BlendEnd;
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);

struct v2f
{
    // ...

    half4 projPos : TEXCOORD0;
};

v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);

    // ...

    o.projPos = ComputeScreenPos(o.pos);
    COMPUTE_EYEDEPTH(o.projPos.z);

    // ...

    return o;
}

fixed4 frag(v2f i) : COLOR
{     
    fixed4 result = 0;
      
    // ... 

    float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos));
    float sceneZ = LinearEyeDepth(depth);
    float partZ = i.projPos.z;
    float fade = saturate( sceneZ - partZ );
    result.a = smoothstep( BlendStart, BlendEnd, fade );

    // ... 
       
    return result; 
}


Sin embargo, este enfoque tiene varias desventajas.

El primer inconveniente está relacionado con el rendimiento. La combinación de profundidad funciona en la etapa de combinación de tuberías de hardware, es decir, inmediatamente después de la rasterización y el cálculo del sombreador de fragmentos. En esta etapa, el resultado de la ejecución del sombreador de fragmentos se mezcla con el resultado registrado en el búfer de salida [3] [4] [5] de acuerdo con la fórmula predefinida por las llamadas a la API [6] [7] [8] [9] .

Esta es la parte menos progresiva de cualquier tubería de hardware en el sentido de que funciona exactamente como su predecesor trabajó hace veinte años. La GPU lee el valor de la memoria, lo mezcla con el valor del sombreador de fragmentos y lo vuelve a escribir en la memoria.

También hay una diferencia en si se debe usar la combinación de profundidad para modelos 3D totalmente transparentes o parcialmente transparentes. Transparente, por ejemplo, carteles de partículas, incluso sin mezclar en profundidad, todo el renderizado es transparente. En el caso de los modelos 3D opacos, la transparencia real, tangible y visible al mezclar en profundidad estará dotada con solo un número muy pequeño de fragmentos, mientras que la gran mayoría de ellos permanecerá opaca. Pero esto último no significa en absoluto que la mezcla no se utilizará para su renderizado, simplemente funcionará inactivo.

El segundo inconveniente está relacionado con cómo se selecciona el color para mezclar. En resumen, todos los fragmentos que se mezclan en un píxel de pantalla particular se encuentran en un rayo que emana de la posición mundial de la cámara y pasa a través de la posición mundial de este píxel de pantalla. Esto, a su vez, significa que con cualquier cambio en la posición u orientación de la cámara, se observará paralaje: los fragmentos del modelo 3D ubicados más cerca de la cámara se moverán más rápido que los fragmentos del paisaje ubicados más lejos de la cámara [10] [11] . Esto es especialmente notable cuando se ve desde cerca con un desplazamiento lateral constante de la cámara.


Paralaje lateral al mover la cámara: los fragmentos del modelo 3D se desplazan a una mayor distancia en comparación con los fragmentos del paisaje.


Paralaje lateral al mover la cámara: al fijar la cámara en un fragmento del paisaje, se nota cuán rápido se mueven los fragmentos del modelo.

Cuando se gira la cámara, se observa paralaje inmediatamente a lo largo de dos ejes de coordenadas de la pantalla. Sin embargo, en dinámica esto es menos evidente que el paralaje lateral.


Paralaje azimutal cuando se cambia la cámara: es más difícil para el cerebro reconocer el patrón de paralaje cuando los fragmentos se desplazan a lo largo de dos

ejes, pero lo más notable es que la apariencia de la fusión en profundidad cambia según el ángulo en el que el observador mira la superficie del paisaje. La zona de fusión se vuelve casi invisible cuando la dirección de visión es perpendicular a la normal a la superficie del paisaje, pero el tamaño de esta zona aumenta rápidamente si inclina la cámara hacia abajo.


Cambiar el ancho de la zona de fusión mientras inclina la cámara

en profundidad La combinación podría ser una buena opción para eliminar las líneas de intersección de los modelos 3D con el paisaje, si no fuera por la abundancia de artefactos que lo acompañan. Este método es más adecuado para efectos de partículas que no son estáticos y, por regla general, no contienen texturas muy detalladas, por lo tanto, no se observan efectos de paralaje en su caso.


Mezcla de mapa de altura


Otra opción para implementar la combinación de paisajes es usar un mapa de altura, al que Unity proporciona acceso a través de la API TerrainData [12] .

Conociendo la posición del objeto Terrain y las dimensiones del terreno indicadas en TerrainData, y teniendo a mano un "mapa de altura", puede calcular la altura del terreno en cualquier punto especificado en las coordenadas mundiales.


Parámetros de terreno necesarios para muestrear el mapa de altura

// Setting up a heightmap and uniforms to use with shaders... 

Shader.SetGlobalTexture(Uniforms.TerrainHeightmap, terrain.terrainData.heightmapTexture);
Shader.SetGlobalVector(Uniforms.HeightmapScale, terrain.terrainData.heightmapScale);
Shader.SetGlobalVector(Uniforms.TerrainSize, terrain.terrainData.size);
Shader.SetGlobalVector(Uniforms.TerrainPos, terrain.transform.position);

Bueno, ahora, después de calcular la altura del paisaje, también puede calcular las coordenadas uv en el sombreador para muestrear el mapa de las alturas del paisaje en coordenadas mundiales.

// Computes UV for sampling terrain heightmap... 

float2 TerrainUV(float3 worldPos)
{
    return (worldPos.xz - TerrainPos.xz) / TerrainSize.xz;
}

Para poder usar el mismo código en fragmentos y sombreadores de vértices, la función tex2Dlod se usa para el muestreo. Además, el mapa de altura no tiene niveles de mip, por lo que muestrearlo con la función tex2D, que calcula automáticamente el nivel de mip, básicamente no tiene sentido.

// Returns the height of terrain at a given position in world space... 

float TerrainHeight(float2 terrainUV)
{
    float heightmapSample = tex2Dlod(TerrainHeightmap, float4(terrainUV,0,0));
    return TerrainPos.y + UnpackHeightmap(heightmapSample) * HeightmapScale.y * 2;
}

Puede intentar reproducir la eliminación de intersecciones a través de la transparencia sin usar un búfer de profundidad. Esto no resuelve otros problemas asociados con este método, pero hace posible verificar la operabilidad de la combinación usando un mapa de altura.

Ver código
// Blending with terrain heightmap

#include "UnityCG.cginc"

float BlendStart;
float BlendEnd;

sampler2D_float TerrainHeightmap; 
float4 HeightmapScale;
float4 TerrainSize;
float4 TerrainPos;        

struct v2f
{
   // ...

   float3 worldPos : TEXCOORD0;
   float2 heightMapUV : TEXCOORD1;

   // ...
};


v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);
   
    // ...
    
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    o.heightMapUV = TerrainUV(o.worldPos);

    // ...

    return o;
}

fixed4 frag(v2f i) : COLOR
{
    fixed4 result = 0;

    // ... 

    half height = TerrainHeight(i.heightMapUV);
    half deltaHeight = i.worldPos.y - height;
    result.a = smoothstep( BlendStart, BlendEnd, deltaHeight );

    // ... 
       
    return result; 
}





Mezcla de profundidad y elevación. El ancho de la zona de mezcla difiere con los mismos parámetros de sombreado

Las ilustraciones usan parámetros de mezcla idénticos para ambos métodos. El ancho de las zonas de fusión es visualmente diferente, ya que la combinación con un mapa de altura no depende del ángulo entre la mirada del observador y la normalidad del paisaje.

La combinación con un mapa de altura es al menos en un aspecto mejor que la combinación en profundidad: corrige la dependencia de la combinación que es visible a simple vista en el ángulo en que la cámara mira el paisaje. Desafortunadamente, el efecto de paralaje aún se observará.


Mezcla de reconstrucción de paisajismo


Para deshacerse del paralaje, debe mezclar un fragmento del modelo 3D con un fragmento del paisaje que esté verticalmente debajo de él (la selección de color para mezclar en este caso no depende de la posición y orientación de la cámara).


Cómo arreglar el paralaje: elegir un fragmento de paisaje para mezclar

Por supuesto, aquí estamos hablando más sobre un fragmento de paisaje virtual. Dependiendo de la posición de la cámara, es posible una situación en la que un fragmento del paisaje, con el que es necesario mezclar un fragmento de un modelo 3D, ni siquiera caiga en el campo de visión de la cámara. Existe un problema similar en la representación de reflexiones locales en el espacio de pantalla (SSLR). Consiste en el hecho de que es imposible representar el reflejo de un fragmento que no está en la pantalla [13] .

En el caso del paisaje, el color del fragmento virtual se puede reconstruir con alta precisión utilizando texturas auxiliares proporcionadas por la API de Unity: mapa normal [14] , mapa de luz [15] , texturas ponderadas para capas de mezcla [16] y texturas incluidas en composición de las capas [17] .


Reconstrucción de un fragmento del paisaje

Todas las texturas que componen el paisaje se muestrean de acuerdo con el mismo UV que el mapa de altura. En el caso de las capas, las coordenadas para el muestreo se ajustan mediante los parámetros de mosaico especificados para una capa particular [18] [19] .

Ver código
// Blending with reconstructed terrain fragments

#include "UnityCG.cginc"

float BlendStart;
float BlendEnd;

sampler2D_float TerrainHeightmapTexture;
sampler2D_float TerrainNormalTexture;
sampler2D TerrainAlphaMap;

float4 HeightmapScale;
float4 TerrainSize;
float4 TerrainPos;
Float4 TerrainLightmap_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap0);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap0);
half4 TerrainSplatMap0_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap1);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap1);
half4 TerrainSplatMap1_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap2);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap2);
half4 TerrainSplatMap2_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap3);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap3);
half4 TerrainSplatMap3_ST;

struct v2f
{
   // ...

   float3 worldPos : TEXCOORD0;
   float2 heightMapUV : TEXCOORD1;
#if defined(LIGHTMAP_ON)
   float2 modelLightMapUV : TEXCOORD2;
   float2 terrainLightMapUV : TEXCOORD3;
#endif

   // ...
};


v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);
   
    // ...
    
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    o.heightMapUV = TerrainUV(o.worldPos);

#if defined(LIGHTMAP_ON)
    o.modelLightMapUV = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
    o.terrainLightMapUV = o.heightMapUV * TerrainLightmap_ST.xy + TerrainLightmap_ST.zw;
#endif

    // ...

    return o;
}
half3 TerrainNormal(float2 terrainUV)
{
    return tex2Dlod( TerrainNormalTexture, float4(terrainUV,0,0) ).xyz * 2.0 - 1.0;
}

half4 TerrainSplatMap(float2 uv0, float2 uv1, float2 uv2, float2 uv3, half4 control)
{
    half4 splat0 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap0, TerrainSplatMap0, uv0);
    half4 splat1 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap1, TerrainSplatMap1, uv1);
    half4 splat2 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap2, TerrainSplatMap2, uv2);
    half4 splat3 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap3, TerrainSplatMap3, uv3);         
    half4 result = splat0 * control.r + 
                   splat1 * control.g + 
                   splat2 * control.b + 
                   splat3 * control.a;
    return result;
}

half3 TerrainNormalMap(float2 uv0, float2 uv1, float2 uv2, float2 uv3, half4 control)
{
    half4 n0 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap0, TerrainSplatMap0, uv0);
    half4 n1 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap1, TerrainSplatMap1, uv1);
    half4 n2 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap2, TerrainSplatMap2, uv2);
    half4 n3 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap3, TerrainSplatMap3, uv3);
    half3 result = UnpackNormalWithScale(n0, 1.0) * control.r +
                   UnpackNormalWithScale(n1, 1.0) * control.g +
                   UnpackNormalWithScale(n2, 1.0) * control.b +
                   UnpackNormalWithScale(n3, 1.0) * control.a;
    result.z += 1e-5;
    return result;
}

half3 TerrainLightmap(float2 uv, half3 normal)
{
#if defined(LIGHTMAP_ON)
#if defined(DIRLIGHTMAP_COMBINED)
    half4 lm = UNITY_SAMPLE_TEX2D(unity_Lightmap, uv);
    half4 lmd = UNITY_SAMPLE_TEX2D_SAMPLER(unity_LightmapInd, unity_Lightmap, uv);
    half3 result = DecodeLightmapRGBM(lm, unity_Lightmap_HDR);
    result = DecodeDirectionalLightmap(result, lmd, normal);
#else
    half4 lm = UNITY_SAMPLE_TEX2D(unity_Lightmap, uv);
    half3 result = DecodeLightmapRGBM(lm, unity_Lightmap_HDR);
#endif                
#else
    half3 result = UNITY_LIGHTMODEL_AMBIENT.rgb;
#endif
    return result;
}

fixed4 frag(v2f i) : COLOR
{
    fixed4 result = 0;

    // ...

    // compute model color and put it to the result

    // ... 

    // reconstruction of terrain fragment

    float2 splatUV0 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap0);
    float2 splatUV1 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap1);
    float2 splatUV2 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap2);
    float2 splatUV3 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap3);

    half4 control = tex2D(_TerrainAlphaMap, i.heightMapUV);
    half4 terrainColor = TerrainSplatMap(splatUV0, splatUV1, splatUV2, splatUV3, control);

    half3 terrainSurfaceNormal = TerrainNormal(i.heightMapUV);
    half3 terrainSurfaceTangent = cross(terrainSurfaceNormal, float3(0,0,1));
    half3 terrainSurfaceBitangent = cross(terrainSurfaceTangent, terrainSurfaceNormal);

    half3 terrainNormal = TerrainNormalMap(splatUV0, splatUV1, splatUV2, splatUV3, control);
    terrainNormal = terrainNormal.x * terrainSurfaceTangent + 
                    terrainNormal.y * terrainSurfaceBitangent + 
                    terrainNormal.z * terrainSurfaceNormal;
    
    half3 terrainLightmapColor = TerrainLightmap(i.heightMapUV, terrainNormal);
    terrainColor *= terrainLightmapColor;

    // blend model color & terrain color

    half height = TerrainHeight(i.heightMapUV);
    half deltaHeight = i.worldPos.y - height;
    half blendingWeight = smoothstep(BlendStart, BlendEnd, deltaHeight);

    result.rgb = lerp(result.rgb, terrainColor, blendingFactor);
       
    return result; 
}


Por lo tanto, la combinación con la reconstrucción de fragmentos de paisaje corrige todos los problemas típicos de la combinación de profundidad y la combinación con un mapa de altura, incluido el paralaje.



Mezcla de reconstrucción de paisajismo


Rendimiento de reconstrucción de fragmentos de terreno


En este punto, es hora de preguntar, ¿cuánto vale este tipo de compromiso? A primera vista, la intensidad de recursos de la reconstrucción de fragmentos de paisajes supera con creces la intensidad de recursos de la mezcla alfa. Para la reconstrucción es necesario realizar con una docena de operaciones de lectura adicionales desde la memoria. Para la mezcla alfa, solo necesita una operación de lectura desde la memoria y una operación de escritura en la memoria.

En realidad, todo dependerá de las características de la plataforma de hardware. La reconstrucción de fragmentos se ve favorecida por la compresión de texturas, el mapeo de mip, la potencia de procesamiento del núcleo de la GPU y las optimizaciones de tuberías de profundidad de hardware específicas (rechazo de profundidad temprano). Y contra la combinación alfa, el hecho ya mencionado anteriormente jugará que es la parte menos progresiva de cualquier GPU.

Sin embargo, siempre hay espacio para la optimización. Por ejemplo, en el caso de la reconstrucción del color del paisaje, la necesidad de esta reconstrucción es solo para una franja estrecha de fragmentos del modelo 3D ubicado a una altura no superior a una cierta altura sobre la superficie del paisaje.

La ramificación dinámica en sombreadores puede dar resultados de rendimiento poco predecibles, pero hay dos puntos que deben tenerse en cuenta:

  1. Se debe omitir los cálculos innecesarios en la ramificación por una condición si esta condición no se cumple en una parte importante de los casos.
  2. . , ( , ), GPU. ― (branch granularity), , , , , . , , . , GPU , , . , GPU, , 1 (PowerVR SGX).


Visualización de diferentes grados de coherencia

En el caso de la reconstrucción de fragmentos, se tienen en cuenta estos dos puntos: la condición de ramificación en la mayoría de los casos permitirá cortar la implementación de operaciones intensivas en recursos para reconstruir el color del paisaje, y esta condición es coherente, con la excepción de un número muy pequeño de fragmentos (en la ilustración, estos son fragmentos que se encuentran en el límite entre las zonas "roja" y "verde").


Coherencia de la reconstrucción de fragmentos de paisaje

Queda por añadir algunos comentarios sobre este método de fusión:

  1. Unity proporciona todas las texturas necesarias solo si el paisaje tiene Draw Instanced habilitado [20] , de lo contrario, el mapa normal no estará disponible, lo que, a su vez, no le permitirá reconstruir correctamente la iluminación del paisaje para mezclar.
  2. Unity API , (base map) . - .
  3. , API (, Metal 16 ).
  4. 3D- , Terrain, SRP.
  5. 3D- , 3D- .
  6. , «» , «» . , «» , . «» .






Al diseñar modelos 3D, es imposible tener en cuenta la variedad de relieves del terreno con los que se supone que deben usarse estos modelos. A menudo, los modelos 3D tienen que estar profundamente "hundidos" en el paisaje o rotados para ocultar las partes sobresalientes, o viceversa, para mostrar las partes ocultas que deberían ser visibles. Los modelos de "calentamiento" limitan su aplicabilidad, y si los modelos 3D se renderizan antes que el paisaje, también produce un efecto de sobregiro. El giro, a su vez, también está lejos de ser adecuado para todos los modelos 3D (por ejemplo, no para casas y árboles).


Para ocultar los elementos sobresalientes del modelo 3D, debe estar "ahogado" en el paisaje.

Ajustar es un término familiar para los usuarios de editores gráficos. Esta es una función que permite que los puntos de control se "peguen" a los nodos de la cuadrícula espacial, y en los editores 3D a las caras y superficies de otros objetos. Ajustar al mapa de las alturas del paisaje en el sombreador de vértices puede simplificar enormemente el diseño de las escenas.


Modelo 3D sin romperse. Modelo 3D con ajuste de vértice. Modelo 3D con ajuste de vértice y fusión. Modelo 3D con ajuste de vértice, fusión e iluminación estática.

La principal dificultad para implementar el ajuste es que necesita averiguar qué vértices del modelo 3D necesita ajustar al mapa de altura y cuáles no valen la pena. Los vértices contienen solo información sobre la naturaleza local de la superficie (que no es suficiente) y no contienen ninguna información sobre su topología (que es necesaria).

Como en otros casos de aplicación, este problema es más fácil de resolver en la etapa de modelado implementando directamente los parámetros necesarios en los vértices. Como tal parámetro, debe elegir un atributo intuitivo, por ejemplo, el factor de ponderación para el ajuste (y no la distancia al borde de una superficie abierta, ya que nos gustaría tener flexibilidad).


Codificación de ponderación para el ajuste

Ver código
// Per-vertex snapping with terrain heightmap

#include "UnityCG.cginc"

sampler2D_float TerrainHeightmapTexture;

float4 HeightmapScale;
float4 TerrainSize;
float4 TerrainPos;

struct v2f
{

   // ...

   float3 worldPos : TEXCOORD0;
   float2 heightMapUV : TEXCOORD1;

   // ...

};

v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);
   
    // ...
    
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    o.heightMapUV = TerrainUV(o.worldPos);
    float snappingWeight = v.color.r;                
    half height = TerrainHeight( o.heightMapUV );                
    o.worldPos.y = lerp( o.worldPos.y, height, snappingWeight );
    o.pos = UnityWorldToClipPos( half4( o.worldPos, 1 ) );

    // ...

    return o;
}


La aplicabilidad del ajuste de vértices está limitada por la correspondencia general entre el terreno y la superficie del modelo 3D. Para compensar sus diferencias significativas, es necesario usar otros métodos más intensivos en recursos, por ejemplo, usar modelos 3D con skinning.


Conclusión


La idea principal que debe extraerse del artículo: cualquier sombreador suficientemente complejo y potencialmente escalable necesita datos básicos. Y la tarea del desarrollador es comprender cómo se puede operar el sistema gráfico: qué datos proporciona, cómo se pueden combinar entre sí y cómo usarlo en sombreadores.

En el caso general, podemos concluir que la única opción para superar el marco mediante el cual las posibilidades de efectos gráficos son limitadas es combinar los resultados de varios sombreadores.


Referencias



All Articles