En un sistema de material basado en gráficos, ¿cómo puedo admitir una variedad de tipos de entrada y salida?


11

ingrese la descripción de la imagen aquí

Estoy tratando de envolver mi cabeza en torno a cómo los sistemas materiales como este , esta aplicación. Estos sistemas potentes y fáciles de usar, similares a gráficos, parecen ser relativamente comunes como un método para permitir que programadores y no programadores creen rápidamente sombreadores. Sin embargo, desde mi experiencia relativamente limitada con la programación de gráficos, no estoy completamente seguro de cómo funcionan.


Antecedentes:

Entonces, cuando he programado sistemas simples de renderizado OpenGL antes, normalmente creo una clase de Material que carga, compila y vincula sombreadores a partir de archivos GLSL estáticos que he creado manualmente. También suelo crear esta clase como un contenedor simple para acceder a las variables uniformes GLSL. Como un simple ejemplo, imagine que tengo un sombreador de vértices básico y un sombreador de fragmentos, con un Texture2D extra uniforme para pasar una textura. Mi clase de Material simplemente cargaría y compilaría esos dos sombreadores en un material, y desde ese momento expondría una interfaz simple para leer / escribir el uniforme Texture2D de ese sombreador.

Para hacer que este sistema sea un poco más flexible, generalmente lo escribo de una manera que me permite intentar pasar uniformes de cualquier nombre / tipo [es decir: SetUniform_Vec4 ("AmbientColor", colorVec4); que establecería el uniforme AmbientColor en un vector 4d particular llamado "colorVec4" si ese uniforme existe en el material] .

class Material
{
    private:
       int shaderID;
       string vertShaderPath;
       string fragSahderPath;

       void loadShaderFiles(); //load shaders from files at internal paths.
       void buildMaterial(); //link, compile, buffer with OpenGL, etc.      

    public:
        void SetGenericUniform( string uniformName, int param );
        void SetGenericUniform( string uniformName, float param );
        void SetGenericUniform( string uniformName, vec4 param );
        //overrides for various types, etc...

        int GetUniform( string uniformName );
        float GetUniform( string uniformName );
        vec4 GetUniform( string uniformName );
        //etc...

        //ctor, dtor, etc., omitted for clarity..
}

Esto funciona, pero se siente como un mal sistema debido al hecho de que el cliente de la clase Material tiene que acceder a los uniformes solo por fe: el usuario debe ser algo consciente de los uniformes que hay en cada objeto material porque se ven obligados a páselos por su nombre GLSL. No es un gran problema cuando solo 1-2 personas trabajan con el sistema, pero no puedo imaginar que este sistema escale muy bien, y antes de hacer mi próximo intento de programar un sistema de renderizado OpenGL, quiero nivelar Subir un poco.


Pregunta:

Ahí es donde estoy hasta ahora, así que he estado tratando de estudiar cómo otros motores de procesamiento manejan sus sistemas de materiales.

Este enfoque basado en nodos es excelente y parece ser un sistema extremadamente común para crear sistemas de materiales fáciles de usar en motores y herramientas modernos. Por lo que puedo decir, se basan en una estructura de datos gráficos donde cada nodo representa un aspecto sombreado de su material y cada ruta representa algún tipo de relación entre ellos.

Por lo que puedo decir, implementar ese tipo de sistema sería tan simple como una clase MaterialNode con una variedad de subclases (TextureNode, FloatNode, LerpNode, etc.). Donde cada subclase de MaterialNode tendría MaterialConnections.

class MaterialConnection
{
    MatNode_Out * fromNode;
    MatNode_In * toNode;
}

class LerpNode : MaterialNode
{
    MatNode_In x;
    MatNode_In y;
    MatNode_In alpha;

    MatNode_Out result;
}

Esa es la idea básica , pero no estoy seguro de cómo funcionarían algunos aspectos de este sistema:

1.) Si observa las diversas 'Expresiones de materiales' (nodos) que utiliza Unreal Engine 4 , verá que cada una tiene conexiones de entrada y salida de una variedad de tipos. Algunos nodos de salida flotantes, algunos de salida vector2, algunos de salida vector4, etc. ¿Cómo puedo mejorar los nodos y las conexiones anteriores para que puedan admitir una variedad de tipos de entrada y salida? ¿Sería una buena elección subclasificar MatNode_Out con MatNode_Out_Float y MatNode_Out_Vec4 (y así sucesivamente)?

2.) Finalmente, ¿cómo se relaciona este tipo de sistema con los sombreadores GLSL? Mirando nuevamente a UE4 (y de manera similar para los otros sistemas vinculados anteriormente), se requiere que el usuario conecte algún nodo de material en un nodo grande con varios parámetros que representan parámetros de sombreado (color base, metalidad, brillo, emisividad, etc.) . Mi suposición original era que UE4 tenía algún tipo de 'sombreador maestro' codificado con una variedad de uniformes, y todo lo que el usuario hace en su 'material' simplemente se pasa al 'sombreador maestro' cuando conectan sus nodos en el ' nodo maestro '.

Sin embargo, la documentación de UE4 establece:

"Cada nodo contiene un fragmento de código HLSL, designado para realizar una tarea específica. Esto significa que a medida que construye un Material, está creando código HLSL a través de secuencias de comandos visuales".

Si eso es cierto, ¿este sistema genera un script de sombreador real? ¿Cómo funciona esto exactamente?


1
Relacionado con tu pregunta: gameangst.com/?p=441
glampert

Respuestas:


10

Trataré de responder lo mejor que pueda, con poco conocimiento sobre el caso específico de UE4, sino más bien sobre la técnica general.

Los materiales basados ​​en gráficos tienen tanta programación como escribir el código usted mismo. Simplemente no se siente así para las personas sin antecedentes en el código, por lo que parece ser más fácil. Entonces, cuando un diseñador vincula un nodo "Agregar", básicamente escribe add (valor1, valor2) y vincula la salida a otra cosa. Esto es lo que significan que cada nodo generará código HLSL, ya sea una llamada de función o simplemente instrucciones directas.

Al final, usar el gráfico de material es como programar sombreadores en bruto con una biblioteca de funciones predefinidas que hacen algunas cosas útiles comunes, y eso es también lo que hace UE4. Tiene una biblioteca de código de sombreador que un compilador de sombreador tomará e inyectará en la fuente de sombreador final cuando corresponda.

En el caso de UE4, si afirman que está convertido a HLSL, supongo que usan una herramienta de conversión que puede convertir el código de bytes HLSL en código de bytes GLSL, por lo que puede usarse en plataformas GL. Pero otras bibliotecas solo tienen múltiples compiladores de sombreadores, que leerán el gráfico y generarán directamente las fuentes de lenguaje de sombreado necesarias.

El gráfico de material también es una buena manera de abstraerse de los detalles de la plataforma y enfocarse en lo que importa desde el punto de vista de la dirección de arte. Dado que no está vinculado a un idioma y a un nivel mucho más alto, es más fácil de optimizar para la plataforma de destino e inyectar dinámicamente otro código como el manejo de la luz en el sombreador.

1) Ahora, para responder a sus preguntas más directamente, debe tener un enfoque basado en datos para diseñar dicho sistema. Encuentre un formato plano que pueda definirse en estructuras muy simples e incluso definirse en un archivo de texto. En esencia, cada gráfico debe ser una matriz de nodos, con un tipo, un conjunto de entradas y salidas, y cada uno de estos campos debe tener un link_id local para asegurarse de que las conexiones del gráfico no sean ambiguas. Además, cada uno de estos campos podría tener una configuración adicional a la que admite el campo (por ejemplo, qué rango de tipos de datos son compatibles).

Con este enfoque, podría definir fácilmente el campo de un nodo para que sea (flotante | doble) y dejar que infiera el tipo de las conexiones, o forzar un tipo en él, sin jerarquías de clase o ingeniería excesiva. Depende de usted diseñar esta estructura de datos gráficos tan rígida o flexible como desee. Todo lo que necesita es que tenga suficiente información para que el generador de código no tenga ambigüedad y, por lo tanto, potencialmente maneje mal lo que desea hacer. Lo importante es que, en el nivel básico de la estructura de datos, lo mantenga flexible y centrado en resolver la tarea de definir un material solo.

Cuando digo "definir un material" me refiero muy específicamente a definir una superficie de malla, más allá de lo que proporciona la geometría. Esto incluye el uso de atributos de vértice adicionales para configurar el aspecto de la superficie, agregarle desplazamiento con un mapa de altura, alterar las normales con normales por píxel, cambiar parámetros basados ​​en la física, cambiar los BRDF, etc. No desea describir nada más como HDR, mapeo de tonos, animación de skinning, manejo de la luz o muchas otras cosas hechas en sombreadores.

2) Entonces depende del generador de sombreadores del renderizador atravesar esta estructura de datos y, al leer su información, reunir un conjunto de variables y vincularlas usando funciones prefabricadas e inyectando el código que calcula la iluminación y otros efectos. Solo recuerde que los sombreadores varían no solo de las diferentes API de gráficos, sino también entre diferentes renderizadores (un renderizado diferido frente a un renderizador basado en mosaico frente a un renderizador directo requieren diferentes sombreadores para funcionar), y con un sistema de materiales como este, puede abstraerse de lo desagradable capa de bajo nivel, y concéntrese solo en describir la superficie.

Para UE4, se les ocurrió una lista de cosas para ese nodo de salida final que mencionas, que creen que describe el 99% de las superficies en juegos antiguos y modernos. Desarrollaron este conjunto de parámetros durante décadas y lo probaron con la increíble cantidad de juegos que el motor Unreal ha producido hasta ahora. Por lo tanto, estará bien si hace las cosas de la misma manera que lo irreal.

Para concluir, sugiero un archivo .material solo para manejar cada gráfico. Durante el desarrollo, contendría quizás un formato basado en texto para depurar y luego se empaquetaría o compilaría en binario para su lanzamiento. Cada .material estaría compuesto por N nodos y N conexiones, muy parecido a una base de datos SQL. Cada nodo tendría N campos, con un nombre y algunos indicadores para los tipos aceptados, si su entrada o salida, si los tipos se infieren, etc. La estructura de datos de tiempo de ejecución para contener el material cargado sería igual de plana y simple, por lo que El editor puede adaptarlo fácilmente y guardarlo nuevamente en el archivo.

Y luego deje el trabajo pesado real para la generación final de sombreadores, que es realmente la parte más difícil de hacer. Lo bueno es que su material se mantiene independiente de la plataforma de renderizado, en teoría funcionaría con cualquier técnica de renderizado y API siempre que represente el material en su lenguaje de sombreado apropiado.

Avíseme si necesita detalles adicionales o alguna solución en mi respuesta, perdí la descripción general de todo el texto.


No puedo agradecerles lo suficiente por escribir una respuesta tan elaborada y excelente. ¡Siento que tengo una gran idea de a dónde debo ir desde aquí! ¡Gracias!
MrKatSwordfish

1
No hay problema amigo, no dude en enviarme un mensaje si necesita más ayuda. De hecho, estoy trabajando en algo equivalente para mis propias herramientas, así que si quieres intercambiar ideas, ¡sé mi invitado! Que tenga una buena tarde: D
Grimshaw
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.