1 puntos por GN⁺ 2024-06-06 | 1 comentarios | Compartir por WhatsApp
  • La luna y los planetas gaseosos del fondo del cielo pueden representar superficie, rotación y atmósfera usando solo un disco circular y un pixel shader, sin necesidad de una malla esférica real
  • La UV sphere tradicional facilita el mapeo de texturas 2×1, pero también introduce contornos angulosos, pérdida por muestreo de mipmaps, patrones repetitivos, seam, distorsión en los polos y límites para el halo atmosférico
  • El enfoque con disco reconstruye la posición de la superficie esférica desde el UV central y luego genera UV esféricas basadas en asin, una matriz de rotación y un spin mediante desplazamiento de la coordenada u, para crear una superficie que se ve como esfera a nivel de píxel
  • El shading parte de una iluminación Lambertiana usando la posición de la superficie como normal, y para superficies rocosas aplica una matriz TBN y normal map; la atmósfera aproxima el brillo del halo y la absorción por canal
  • Las texturas estáticas de Substance Designer y las render textures dinámicas de Unreal pueden tratarse de la misma manera, lo que permite reducir el costo de rendimiento usando texturas dinámicas más pequeñas y ajustando el tiling

Objetivo visual de los planetas del skybox

  • El skybox del proyecto se enfoca en crear una composición de cielo alienígena con una luna animada y un planeta gaseoso
  • Ambos planetas rotan, y el planeta gaseoso también incluye flujos atmosféricos en movimiento
    • El movimiento real es sutil, pero se acelera por efecto visual
  • La dirección artística sigue un estilo semirrealista (semi-realistic) inspirado en el arte de ciencia ficción de los años 70 y 80

Entradas de textura de superficie y limitaciones

  • La superficie del planeta gaseoso se genera en tiempo real con un pixel shader y un flujo de render-to-texture
  • La textura de la luna se crea en Substance Designer, y se pueden probar distintos looks cambiando la paleta de colores y la material seed
  • El planeta gaseoso dinámico exige renderizar la textura en cada frame, así que el costo de rendimiento es alto
    • Si la resolución de la textura se duplica, el costo de renderizado se cuadruplica
  • Se necesita una solución que funcione de forma consistente tanto con texturas estáticas de Substance Designer como con texturas renderizadas dinámicamente en Unreal
  • Al inicio se consideró usar cubemap, pero puede consumir 6 veces más recursos que una textura cuadrada y Substance Designer no soporta generar cubemaps, así que no resultó práctico

Limitaciones encontradas con la UV sphere

  • La UV sphere parece la opción predeterminada porque es fácil alinearla con una textura rectangular 2×1 y sus coordenadas
  • Como solo se ve una cara de la esfera a la vez, en vez de una textura rectangular única para toda la esfera también se podría usar una textura cuadrada con tiling
  • En la práctica aparecen varios problemas al mismo tiempo
    • El contorno se ve notablemente anguloso, y hay que ocultarlo aumentando la subdivisión o con una máscara en el pixel shader
    • La mayor parte de la superficie esférica se ve en ángulo, así que se muestrea desde mipmaps de baja resolución; en texturas dinámicas, una parte importante de los píxeles generados en cada frame termina usándose en zonas casi invisibles
    • El tiling debe aumentarse en múltiplos enteros para que el patrón completo continúe, y aun con un factor de 2 la repetición ya se vuelve evidente
    • El tiling fraccional crea un seam que va del polo norte al polo sur
    • En los polos aparecen pinching de textura y distorsión del mapeo triangular, parecida a las texturas temblorosas de PSX
    • Como solo se puede dibujar la superficie, el halo atmosférico requiere un modelo aparte
  • La UV sphere sigue siendo útil para modelar planetas, pero en un skybox termina necesitando demasiadas correcciones y hacks

Enfoque con disco circular y pixel shader

  • Como los planetas del skybox se observan desde lejos y desde una sola posición, no hace falta una malla esférica 3D compleja
  • Se usa un disco poligonal circular relleno simple, y el pixel shader se encarga del mapeo de textura
  • En la misma malla también se puede dibujar el halo atmosférico
  • El origen UV del disco debe estar exactamente en el centro
    • Si se toma el radio del planeta como 1, las coordenadas UV deben extenderse más allá para dejar espacio al dibujo de la atmósfera

Reconstrucción de la superficie esférica

  • El sistema de coordenadas usa una convención left-handed, Y-up consistente con DirectX
    • Unreal Engine también es left-handed, pero su eje up es Z, así que hay que revisar la orientación y el formato del normal map
  • La ecuación de la superficie mapea una posición 2D del plano del disco a una posición 3D sobre la superficie de la esfera
  • Primero se verifica si el píxel está dentro de la superficie esférica comprobando si la longitud del vector UV es menor que el radio
float CircleMask( float2 uv, float radius)
{
    return length(uv) < radius? 1.0: 0.0;
}
  • Este método solo funciona correctamente si las coordenadas UV están alineadas con el centro de la malla
  • La local position de la superficie esférica usa directamente x y y del UV, y solo reconstruye z
float3 ReconstructSurface(float2 uv)
{
    float zSquared = 1.0 - dot(uv, uv);
    float z = sqrt(zSquared);
    return float3(uv, z);
}

Generación de UV esféricas

  • El proceso de mapear una textura cuadrada sobre la superficie esférica se divide en tres pasos
    • Envolverla sobre un cilindro
    • Repetir el mismo tratamiento sobre el eje y
    • Deformar el resultado hasta darle forma circular
  • La coordenada x de la textura es proporcional al ángulo con el que se envuelve el cilindro; se calcula arcsine(x) y luego se vuelve a mapear [-π, π] a [0, 1]
  • Se usa la generatriz como ancho local de la esfera para aplicar pinching de coordenadas en los polos
float2 GenerateSphericalUV(float3 position)
{
    float width = sqrt(1.0 - position.y * position.y);
    float generatrixX = position.x / width * sign(position.z);
    float2 generatrix = float2(generatrixX, position.y);
    float2 uv = asin(generatrix) / 3.14159 + float2(0.5, 0.5);
    return float2(uv);
}

Inclinación del eje y rotación

  • Para lograr una composición planetaria más natural, el eje del planeta se inclina con pitch y roll
    • En el arte sci-fi los planetas suelen estar inclinados, lo que vuelve la composición más dinámica y permite mostrar casquetes polares
  • El yaw corresponde al movimiento de rotación y se maneja en una etapa separada para evitar problemas con el seam
  • En una malla esférica normalmente se transforma el planeta con una matriz, pero en el enfoque con disco cada píxel define su posición en la superficie, así que basta con una matriz 3×3 que contenga solo la rotación
float3x3 CreateRotationMatrix(float pitch, float roll) {
    float cosPitch = cos(pitch);
    float sinPitch = sin(pitch);
    float cosRoll = cos(roll);
    float sinRoll = sin(roll);

    return (float3x3)(
        cosRoll, -sinRoll * cosPitch, sinRoll * sinPitch,
        sinRoll, cosRoll * cosPitch, -cosRoll * sinPitch,
        0.0, sinPitch, cosPitch
    );
}
  • El material editor de Unreal no soporta matrices como tipo de dato, pero hay formas de resolverlo

Manejo de scale, seam y spin

  • Al cambiar la escala de la textura aparece el UV seam que antes quedaba oculto en el borde original de la textura
  • El seam no se puede eliminar del todo con facilidad, pero si se mueve hacia atrás resulta menos visible
  • La esfera se divide en cuadrantes según el signo de la posición, y se aplica un offset en dirección u a los cuadrantes back-left y back-right
float2 GenerateSphericalUV(float3 position, float spin, float scale)
{
    float leftRightSign = sign(position.x);
    float frontBackSign = sign(position.z);
    float width = sqrt(1.0 - position.y * position.y);
    float generatrixX = position.x / width * frontBackSign;
    float2 generatrix = float2 (generatrixX, position.y);
    float2 uv = asin(generatrix) / 3.14159 + float2 (spin, 0.5);
    if(frontBackSign < 0.0)
    {
        uv = float2 (uv.x + 1.0 * leftRightSign, uv.y);
    }
    return float2 (uv / scale);
}
  • El seam sigue conectando el polo norte con el polo sur, pero al moverse hacia atrás se nota mucho menos
  • La rotación se maneja desplazando la textura de la superficie en la dirección de la coordenada u, en lugar de rotar la esfera en sí
float2 GenerateSphericalUV(float3 position, float scale, float spin)
{
    float width = sqrt(1.0 - position.y * position.y);
    float generatrixX = position.x / width * sign(position.z);
    float2 generatrix = float2(generatrixX, position.y);
    float2 uv = asin(generatrix) / 3.14159 * scale + float2(0.5 + spin, 0.5);
    return float2(uv);
}
float2 sphericalVU = GenerateSphericalUV(position, scale, time*speed)

Shading de superficie y normal map

  • El shading es clave para que el planeta se vea redondo
  • A diferencia del shading normal del motor, el planeta tiene dos capas, superficie y atmósfera, así que la iluminación y el blending de ambas deben definirse manualmente
  • Si el radio es 1, la posición de la superficie antes de la rotación puede usarse directamente como surface normal
  • La iluminación usa un modelo Lambertiano simple
    • La meta no es el realismo total, sino una estética de portada sci-fi estilizada inspirada en fotos de la NASA
float LambertianLight(float3 normal, float3 lightDirection) {
    float NdotL = max(dot(normal, lightDirection), 0.0);
    return NdotL;
}
  • En superficies suaves como un gas giant puede bastar con la normal simple
  • Para planetas rocosos conviene simular relieve como montañas, crestas y cráteres mediante normal map
  • Para usar normal map hace falta una matriz TBN compuesta por Tangent, Bitangent y Normal
    • En una esfera este cálculo debe hacerse por píxel
    • La matriz TBN se usa para convertir la normal del normal map a World Space Coordinates

Discontinuidad de UV y artefactos de mipmap

  • Las UV generadas en el pixel shader pueden provocar problemas de discontinuidad
  • En el seam trasero, el componente UV horizontal debería conectar perfectamente entre 0.0 y 1.0 en el borde, pero en la práctica aparecen artefactos blocky a lo largo del seam
  • Esta línea está relacionada con el DDX de la UV
    • DDX y DDY miden la tasa de cambio de la UV en los ejes del espacio de pantalla
    • El sampler de textura usa esos valores para decidir qué mipmap utilizar
    • Derivadas UV bajas corresponden a mipmaps de alta resolución, y derivadas altas a mipmaps de baja resolución
  • Si los valores a ambos lados del seam saltan bruscamente, el DDX se vuelve grande y puede terminar muestreando el mipmap más bajo, visible como una línea gris
  • En este caso no es un problema grave porque se planea cubrirlo con un polar patch, pero si llega a notarse hay que corregir manualmente el DDX y el DDY del seam y pasárselos al sampler
  • El fix mostrado es solo ilustrativo; en un caso real hay que derivar la fórmula según el mapeo concreto

Parches polares y corrección del pinching

  • La distorsión en los polos se cubre con el método probado del polar patch
  • En planetas o lunas, las regiones polares suelen estar cubiertas de hielo y verse distintas del resto, así que un parche de textura aparte no solo oculta el problema sino que además aporta interés visual
  • El polar patch puede añadirse mapeándolo sobre un plano perpendicular al eje de rotación y luego rotando las coordenadas resultantes
float2 PolarPatchMapping(float3 position, float scale, float spin)
{
    float cosSpin = cos(spin);
    float sinSpin = sin(spin);
    float scale = 0.4;

    float2 uv = float2(position.x, position.z) * scale;
    float2 spinningUV = float2(uv.x * cosSpin - uv.y * sinSpin,
                               uv.x * sinSpin + uv.y * cosSpin);

    return spinningUV;
}
  • Como el mapa UV no se guarda en vértices sino que se genera por píxel, hay más flexibilidad para corregir el pinching en los polos
  • Una división simple permite corregir el aspect ratio de la textura polar
uv = float2(uv.x, uv.y/pow(width, 1/3));
  • El resultado de la corrección puede verse como otro tipo de artifact, pero en una escena realista conecta mejor con el polar patch y reduce la inconsistencia visual

Aproximación del halo atmosférico

  • Como la superficie del planeta no ocupa toda el área del disco, el espacio restante puede usarse para dibujar un halo atmosférico
  • Dibujar el halo dentro del mismo shader permite integrarlo de forma natural con la atmósfera sobre el planeta
  • El renderizado atmosférico basado en física suele usar Rayleigh scattering y raymarching, pero aquí no se usa
  • En cambio, se aplica una aproximación parecida al truco de iluminación para objetos dinámicos en Quake 1 y 2
    • La iluminación estática de Quake se precalculaba en lightmaps
    • Los monstruos dinámicos ajustaban su color muestreando el lightmap de abajo; no era físicamente correcto, pero sí lograba que encajaran con el entorno
  • De la misma forma, la superficie del planeta se usa para aproximar la iluminación del halo atmosférico
    • Se expande la normal de la superficie antes del bump mapping para calcular el brillo del halo
    • Remapear el brillo de forma distinta por canal permite simular de manera simple la absorción diferente según la longitud de onda de la luz
    • Como la atmósfera se ve mejor en ángulos rasantes, se remapea el componente Z de la surface normal
    • También se calcula la distancia desde la superficie esférica para crear el efecto de desvanecimiento del halo
  • Al final, superficie y atmósfera se combinan con alpha blending

Resultado final y uso práctico

  • Hicieron falta más pasos de los esperados, pero el resultado final se ve como una esfera completa y permite las manipulaciones deseadas de textura y shader
  • Con un solo graph de Substance Designer se puede crear la textura de la superficie, sin necesidad de posprocesado adicional en el resultado
  • Las texturas animadas también pueden manejarse del mismo modo
    • Se pueden usar texturas más pequeñas para ahorrar rendimiento
    • El tiling puede ajustarse según haga falta
  • El video del resultado puede verse en Video 3

1 comentarios

 
GN⁺ 2024-06-06
Opiniones en Hacker News
  • Me parece que el autor descartó los cubemaps demasiado rápido; cuando rendericé un planeta gaseoso dinámico en un proyecto personal, resultó ser la solución más simple.
    Usar cubemaps no multiplica la memoria por 6: es dividir una textura rectangular grande en 6 caras rectangulares más pequeñas, así que el detalle total de la textura es el mismo.
    La ventaja es que no tienes que preocuparte por el aplastamiento en los polos, y con una función de ruido 3D o 4D es fácil crear un campo de flujo sin costuras para animar/distorsionar la textura.
    https://www.junkship.net/News/2016/06/09/jupiter-jazz

    • Para evitar artefactos raros en las costuras, puede ser mejor usar dos hemisferios con proyección estereográfica.
      Cada hemisferio se proyecta como un disco, pero se puede llenar la textura hasta el borde cuadrado y duplicar parte de cada hemisferio en las esquinas de la textura del otro lado.
      Hay una diferencia de escala de 1:2 entre el centro y el borde del disco, así que podría considerarse que se desperdician píxeles tomando como referencia el detalle mínimo; pero como es una proyección conforme, hace mucho menos delicado el muestreo del color del píxel destino en perspectivas pronunciadas, y los cálculos directo/inverso también requieren solo una división y unas cuantas sumas y multiplicaciones por punto proyectado, incluso más barato que la proyección central de un cubemap.
      Si necesitas un método conforme que reduzca aún más la variación de escala y el desperdicio de píxeles en las esquinas sin volverse conceptualmente demasiado difícil, puedes cubrir la esfera como dos piezas de cuero de una pelota de béisbol usando dos proyecciones de Mercator perpendiculares entre sí y con un leve solapamiento.
      Cada pieza puede ser una textura rectangular, y hay artículos de NOAA que proponen este enfoque para la cuadrícula de ecuaciones diferenciales en simulaciones meteorológicas de la Tierra.
      La proyección más eficiente en píxeles que conozco consiste en dividir la esfera en un octaedro y luego cubrir cada octante con una cuadrícula de píxeles hexagonales basada en “coordenadas de área esférica”.
      Cada octante puede representarse en una imagen común de píxeles cuadrados como medio cuadrado, es decir, un triángulo rectángulo 45-45-90, lo que da resultados como https://observablehq.com/@jrus/sac-quincuncial, y también se pueden usar cuadrículas hexagonales como https://observablehq.com/@jrus/sphere-resample.
      Sin embargo, los detalles al tener que muestrear cruzando los bordes son mucho más engorrosos que con el método de dos proyecciones estereográficas, y también pueden aparecer artefactos en las costuras.
    • https://en.wikipedia.org/wiki/Peirce_quincuncial_projection
    • Si quieres reducir aún más la distorsión, también puedes agrupar las caras triangulares de un icosaedro en 10 rombos y cubrir cada rombo con una textura cuadrada.
      Requiere más matemáticas, pero si no planeas subdividir más la superficie por otros motivos, normalmente no vale tanto la pena.
    • Podría probar ese ruido curl para generar nubes en el juego a escala planetaria que estoy haciendo.
  • El aplastamiento de la textura en los polos en realidad es una manifestación extrema de una distorsión que existe en toda la superficie.
    Normalmente solo es evidente en los polos, pero si subdivides adaptativamente una esfera en triángulos, la distorsión también cambia cuando cambia el nivel de subdivisión, así que puede verse en otros lugares.
    El problema es que la esfera se divide en cuadriláteros y cada cuadrilátero se representa en el espacio UV como 2 triángulos con la misma área, pero en el espacio 3D uno de los triángulos —en particular el que tiene el borde horizontal más cercano al polo— es más pequeño.
    Aun así, las UV se interpolan linealmente dentro del triángulo, por lo que la mitad de la textura se encoge y la otra mitad se estira.
    En el polo, el área 3D de uno de los triángulos se vuelve realmente 0, de modo que solo se renderiza la mitad de la textura y la costura entre triángulos se vuelve evidente.
    La solución correcta es calcular las coordenadas UV por píxel en el pixel shader, en lugar de usar interpolación lineal por vértice; si se hace bien, también maneja los polos sin costuras.

    • Estoy empezando a tocar un poco renderizado, shaders y desarrollo de juegos; si alguien tiene pistas, palabras clave o links para investigar más, me gustaría saberlos.
    • ¿Esto no se arregla si, en los componentes XYZ, en vez de dejar W en 1, se escala W?
    • ¿Hay casos en los que se renderice una esfera con varios polos de renderizado para reducir este tipo de aplastamiento?
      Por ejemplo, al renderizar una esfera con un polo de “norte verdadero”, usar el polo de renderizado del norte verdadero cuando se la ve de lado, y usar un renderizado ecuatorial de 0',0’ cuando se la ve desde cerca del norte verdadero.
  • Me dieron ganas de volver a mirar el displacement mapping.
    No sería un sustituto para el problema que intenta resolver el autor, pero es más simple y bastante divertido.
    Hace unos 25 años hice un visualizador musical llamado “Eclipse” para SoundJam; la entrada era un arreglo de niveles del rango de frecuencias audibles y los canales izquierdo/derecho.
    El objetivo era que pareciera un sol eclipsado con emisiones de corona, y los datos de la música se convertían en esas “emisiones”.
    La frecuencia de los datos determinaba en qué parte del contorno del disco solar aparecían.
    Con el tiempo, las emisiones se alejaban del sol y se “enfriaban” hasta desaparecer en negro; una señal fuerte empezaba en blanco y, al debilitarse, pasaba por amarillo, naranja, rojo y café.
    Tenía que mantener un arreglo de valores de datos de sonido en un búfer circular lo bastante grande como para cubrir el tiempo antes de que las emisiones desaparecieran en negro.
    Todo “Eclipse” y la visualización de las emisiones eran, al final, un mapa de desplazamiento, y había precalculado un bitmap en el que el valor de cada píxel era un offset dentro del búfer de niveles de sonido.
    Los píxeles cercanos a la superficie del sol tenían offsets hacia los datos recién entrantes, y los píxeles más externos tenían offsets hacia la cola del búfer, que estaba por expirar.
    Mapear emisiones circulares/radiales a coordenadas cartesianas requirió algo de matemáticas para generar los valores de desplazamiento.
    El bucle principal recibía nuevos valores de sonido, sobrescribía los más antiguos, recorría el mapa de desplazamiento por filas y columnas, obtenía los datos de sonido correspondientes y los mapeaba a colores de una paleta fija para ponerlos en el búfer de visualización.
    No era tan llamativo como otros visualizadores, pero tenía una belleza tranquila y reflejaba bastante bien los datos musicales.
    No se sentía como los visualizadores posteriores, que no podían quedarse quietos ni siquiera cuando les ponías silencio.

  • El artículo está genial, pero a medida que haces scroll hacia abajo carga muchos shaders, así que si no tienes una computadora muy potente, el navegador casi puede congelarse.

    • ¿Qué tan “muy potente” tiene que ser una computadora? ¿Es difícil renderizar algo así incluso con la gráfica integrada de una laptop de menos de 10 años?
  • Es interesante ver cuán diferente interpreta la gente el “realismo”
    En los juegos de una época en la que los recursos eran mucho más limitados, esta diferencia era especialmente evidente, y algunos juegos, aun con gráficos cuadrados, pixelados y de paleta reducida, lograban más inmersión que los juegos actuales de gran presupuesto

    the gas giant’s surface texture will be generated in realtime using a pixel
    shader and a render-to-texture approach
    Es interesante ver cómo se arranca desde el principio con un enfoque poco práctico y sin límites, se encuentra una forma de provocar cuellos de botella de rendimiento, y se sigue bajando por la madriguera
    Me pregunto si al final el autor del post original logró hacerlo funcionar
    Quizá sea la diferencia entre un proyecto con restricciones reales y un proyecto de hobby
    Para el planeta gaseoso, se podría usar una textura animada prerenderizada, seguir con el resto del proyecto y, cuando todo esté funcionando, volver entonces con un cosplay de Slartibartfast

    • Tal vez los juegos de menor fidelidad resultaban más inmersivos porque dejaban espacio para que interviniera la imaginación
      Creo que por eso uno se involucraba de forma más activa
  • Me recordó a este artículo, que trata un enfoque parecido: https://bgolus.medium.com/rendering-a-sphere-on-a-quad-13c92...

  • ¿No existe en la GPU o en las bibliotecas 3D alguna función que recorra horizontalmente cada fila de un círculo y mapee las X,Y de la textura a una posición “3D” sobre la esfera?
    Una esfera es un círculo y los algoritmos para dibujar círculos son simples, pero rotar y proyectar un millón de vértices para triángulos parece un desperdicio enorme de recursos
    Si se recorre en horizontal y luego se baja una línea para seguir dibujando, se pueden dibujar todas las líneas
    Recuerdo haber hecho algo así a fines de los 80 para precalcular cosas como la lente de aumento de Second Reality

    • Probablemente sea porque la interpolación lineal esférica es más lenta y más especializada que la interpolación lineal
      Todo el pipeline de renderizado ya está construido para manejar interpolación lineal
      Con un solo programa shader y 1 triángulo también se puede dibujar una esfera con antialiasing perfecto
    • La GPU no rasteriza línea por línea como los antiguos renderizadores por software; básicamente solo sabe hacer rasterización de triángulos
      Si se renderiza en GPU, pese a su complejidad aparente, el enfoque del artículo se ve bastante bueno
      Hay una excelente serie de artículos sobre la estructura del renderizado basado en GPU: https://fgiesen.wordpress.com/2011/07/09/a-trip-through-the-...
      La parte 6 trata sobre rasterización
    • Con el código que me dio GPT, en cada píxel dentro del círculo se calculaba z, luego se rotaba, y después se obtenían las coordenadas de textura con atan2 y asin para pintar el color; al ejecutarlo parecía funcionar
  • Los planetas impostores con pixel shader 2D sobre el fondo ayudan muchísimo en trabajos con muchos planetas, como en un universo procedural
    Por memoria y ancho de banda de GPU, los planetas basados en cubo esférico se limitan a 1 instancia, y los cuerpos celestes cercanos se dibujan con una técnica de fondo
    En el espacio hay una distancia, más o menos a mitad de camino entre dos cuerpos, en la que ambos se renderizan como impostores; después, el cuerpo más cercano se procesa con un quadtree esférico
    No es perfecto, pero la ilusión es casi irreprochable
    Además, como está sobre un plano, resulta fácil añadir efectos físicos ligeros de lente de cámara para galaxias o cuerpos celestes lejanos
    Por ejemplo, que la Luna parezca más grande de lo real al salir, o que una galaxia distante parezca curvada por la gravedad

  • Me gusta la página relacionada con planetas gaseosos: https://emildziewanowski.com/flowfields/

  • Una icosfera se puede desplegar de forma mucho más suave, y tanto las posiciones de los vértices como los tamaños de los elementos son regulares
    Desplegarla no es trivial, pero se puede hacer