Game Engine Design - Ubershader - Diseño de gestión de sombreadores [cerrado]

18

Quiero implementar un sistema flexible de Ubershader, con sombreado diferido. Mi idea actual es crear sombreadores a partir de módulos, que se ocupan de ciertas características, como FlatTexture, BumpTexture, Displacement Mapping, etc. También hay pequeños módulos que decodifican el color, hacen mapeo de tonos, etc. Esto tiene la ventaja de que puedo reemplazar ciertos tipos de módulos si la GPU no los admite, para que pueda adaptarme a las capacidades actuales de la GPU. No estoy seguro de si este diseño es bueno. Me temo que podría hacer una mala elección de diseño, ahora, y luego pagarla.

Mi pregunta es ¿dónde encuentro recursos, ejemplos, artículos sobre cómo implementar un sistema de administración de sombreadores de manera efectiva? ¿Alguien sabe cómo los motores de los grandes juegos hacen esto?

Michael Staud
fuente
3
No es suficiente para obtener una respuesta real: este enfoque le irá bien si comienza de a poco y deja que crezca orgánicamente según sus necesidades en lugar de tratar de construir el MegaCity-One de sombreadores por adelantado. En primer lugar, mitiga su mayor preocupación de hacer demasiado diseño por adelantado y pagarlo más tarde si no funciona, en segundo lugar, evita hacer un trabajo adicional que nunca se usa.
Patrick Hughes
Desafortunadamente, ya no aceptamos preguntas de "solicitud de recursos".
Gnemlock

Respuestas:

23

Un enfoque semi-común es hacer lo que yo llamo componentes de sombreador , similar a lo que creo que estás llamando módulos.

La idea es similar a un gráfico de posprocesamiento. Usted escribe fragmentos de código de sombreador que incluye las entradas necesarias, las salidas generadas y luego el código para que realmente funcione en ellas. Tiene una lista que indica qué sombreadores aplicar en cualquier situación (si este material necesita un componente de mapeo de relieve, si el componente diferido o directo está habilitado, etc.).

Ahora puede tomar este gráfico y generar código de sombreador a partir de él. Esto significa principalmente "pegar" el código de los fragmentos en su lugar, con el gráfico asegurándose de que ya estén en el orden necesario, y luego pegando las entradas / salidas del sombreador según corresponda (en GLSL, esto significa definir su "global" en , out y variables uniformes).

Esto no es lo mismo que un enfoque ubershader. Ubershaders es donde pones todo el código necesario para todo en un solo conjunto de sombreadores, tal vez usando #ifdefs y uniformes y similares para activar y desactivar las funciones al compilarlas o ejecutarlas. Yo personalmente desprecio el enfoque de ubershader, pero algunos motores AAA bastante impresionantes los usan (Crytek en particular me viene a la mente).

Puede manejar los fragmentos de sombreador de varias maneras. La forma más avanzada, y útil si planea admitir GLSL, HLSL y las consolas, es escribir un analizador para un lenguaje de sombreado (probablemente lo más cerca posible de HLSL / Cg o GLSL para que los desarrolladores puedan "entenderlo" al máximo) ) que luego se puede usar para traducciones de fuente a fuente. Otro enfoque es simplemente envolver fragmentos de sombreado en archivos XML o similares, p. Ej.

<shader name="example" type="pixel">
  <input name="color" type="float4" source="vertex" />
  <output name="color" type="float4" target="output" index="0" />
  <glsl><![CDATA[
     output.color = vec4(input.color.r, 0, 0, 1);
  ]]></glsl>
</shader>

Tenga en cuenta que con ese enfoque puede crear múltiples secciones de código para diferentes API o incluso versionar la sección de código (para que pueda tener una versión GLSL 1.20 y una versión GLSL 3.20). Su gráfico incluso puede excluir automáticamente fragmentos de sombreado que no tienen una sección de código compatible para que pueda obtener una degradación semi-elegante en el hardware más antiguo (por lo que algo como el mapeo normal o lo que sea solo se excluye en el hardware más antiguo que no puede admitirlo sin que el programador lo necesite) hacer un montón de verificaciones explícitas).

La muestra XMl puede generar algo similar a (disculpas si esto es inválido GLSL, ha pasado un tiempo desde que me sometí a esa API):

layout (location=0) in vec4 input_color;
layout (location=0) out vec4 output_color;

struct Input {
  vec4 color;
};
struct Output {
  vec4 color;
}

void main() {
  Input input;
  input.color = input_color;
  Output output;

  // Source: example.shader
#line 5
  output.color = vec4(input.color.r, 0, 0, 1);

  output_color = output.color;
}

Podría ser un poco más inteligente y generar un código más "eficiente", pero, sinceramente, cualquier compilador de sombreadores que no sea una basura total eliminará las redundancias de ese código generado por usted. Tal vez GLSL más reciente también le permite poner el nombre del archivo en los #linecomandos, pero sé que las versiones anteriores son muy deficientes y no lo admiten.

Si tiene varios fragmentos, sus entradas (que no son suministradas como salida por un fragmento ancestro en el árbol) se concatenan en el bloque de entrada, al igual que las salidas, y el código simplemente se concatena. Se realiza un poco de trabajo adicional para garantizar que las etapas coincidan (vértice vs fragmento) y que los diseños de entrada de atributos de vértice "simplemente funcionen". Otro buen beneficio con este enfoque es que puede escribir índices de enlace de atributos de entrada y uniformes explícitos que no son compatibles con versiones anteriores de GLSL y manejarlos en su biblioteca de generación / enlace de sombreadores. Del mismo modo, puede usar los metadatos para configurar sus VBO y glVertexAttribPointerllamadas para garantizar la compatibilidad y que todo "simplemente funcione".

Desafortunadamente, ya no hay una buena biblioteca de API cruzada como esta. Cg se acerca un poco, pero tiene soporte de basura para OpenGL en tarjetas AMD y puede ser extremadamente lento si utiliza cualquiera de las características de generación de código más básicas. El marco de efectos de DirectX también funciona, pero por supuesto no tiene soporte para ningún idioma además de HLSL. Hay algunas bibliotecas incompletas / con errores para GLSL que imitan las bibliotecas de DirectX pero dado su estado la última vez que verifiqué, simplemente escribiría la mía.

El enfoque de ubershader solo significa definir directivas de preprocesador "bien conocidas" para ciertas características y luego volver a compilar para diferentes materiales con diferentes configuraciones. por ejemplo, para cualquier material con un mapa normal que pueda definir USE_NORMAL_MAPPING=1y luego en su ubershader de etapa de píxeles solo tenga:

#if USE_NORMAL_MAPPING
  vec4 normal;
  // all your normal mapping code
#else
  vec4 normal = normalize(in_normal);
#endif

Un gran problema aquí es manejar esto para HLSL precompilado, donde necesita precompilar todas las combinaciones en uso. Incluso con GLSL, debe poder generar correctamente una clave de todas las directivas de preprocesador en uso para evitar recompilar / almacenar en caché sombreadores idénticos. El uso de uniformes puede reducir la complejidad, pero a diferencia de los uniformes del preprocesador, no reduce el recuento de instrucciones y aún puede tener un impacto menor en el rendimiento.

Para ser claros, ambos enfoques (además de escribir manualmente una tonelada de variaciones de sombreadores) se utilizan en el espacio AAA. Use lo que sea mejor para usted.

Sean Middleditch
fuente