Compartir código entre múltiples sombreadores GLSL

30

A menudo me encuentro copiando código entre varios sombreadores. Esto incluye ciertos cálculos o datos compartidos entre todos los sombreadores en una sola tubería, y cálculos comunes que todos mis sombreadores de vértices necesitan (o cualquier otra etapa).

Por supuesto, esa es una práctica horrible: si necesito cambiar el código en cualquier lugar, necesito asegurarme de cambiarlo en cualquier otro lugar.

¿Existe una mejor práctica aceptada para mantener SECO ? ¿Las personas simplemente anteponen un único archivo común a todos sus sombreadores? ¿Escriben su propio preprocesador rudimentario de estilo C que analiza las #includedirectivas? Si hay patrones aceptados en la industria, me gustaría seguirlos.

Martin Ender
fuente
44
Esta pregunta puede ser un poco controvertida, porque varios otros sitios de SE no quieren preguntas sobre las mejores prácticas. Esto es intencional para ver cómo se encuentra esta comunidad con respecto a tales preguntas.
Martin Ender
2
Hmm, me queda bien. Yo diría que en gran medida somos un poco "más amplios" / "más generales" en nuestras preguntas que, por ejemplo, StackOverflow.
Chris dice que reinstala a Mónica el
2
StackOverflow pasó de ser un 'pregúntanos' a un 'no nos preguntes a menos que tengas que complacer'.
dentro del
Si está destinado a determinar el tema, entonces ¿qué tal una Meta pregunta asociada?
SL Barth - Restablece a Monica el
2
Discusión sobre meta.
Martin Ender

Respuestas:

18

Hay muchos enfoques, pero ninguno es perfecto.

Es posible compartir código mediante la glAttachShadercombinación de sombreadores, pero esto no permite compartir cosas como declaraciones de estructura o #defineconstantes -d. Funciona para compartir funciones.

A algunas personas les gusta usar el conjunto de cadenas que se pasan glShaderSourcecomo una forma de anteponer definiciones comunes antes de su código, pero esto tiene algunas desventajas:

  1. Es más difícil controlar lo que debe incluirse desde el sombreador (necesita un sistema separado para esto).
  2. Significa que el autor del sombreador no puede especificar el GLSL #version, debido a la siguiente declaración en la especificación GLSL:

La directiva #version debe aparecer en un sombreador antes que cualquier otra cosa, excepto los comentarios y los espacios en blanco.

Debido a esta declaración, glShaderSourceno se puede usar para anteponer texto antes de las #versiondeclaraciones. Esto significa que la #versionlínea debe incluirse en sus glShaderSourceargumentos, lo que significa que su interfaz de compilador GLSL necesita saber de alguna manera qué versión de GLSL se espera que se use. Además, al no especificar a #version, el compilador GLSL usará GLSL versión 1.10 de forma predeterminada. Si desea permitir que los autores de sombreadores especifiquen #versiondentro del script de una manera estándar, entonces debe insertar #include-s de alguna manera después de la #versiondeclaración. Esto podría hacerse analizando explícitamente el sombreador GLSL para encontrar la #versioncadena (si está presente) y hacer sus inclusiones después de ella, pero teniendo acceso a un#includeLa directiva podría ser preferible para controlar más fácilmente cuando esas inclusiones deben hacerse. Por otro lado, dado que GLSL ignora los comentarios antes de la #versionlínea, puede agregar metadatos para incluirlos dentro de los comentarios en la parte superior de su archivo (qué asco).

La pregunta ahora es: ¿hay una solución estándar para #include, o necesita rodar su propia extensión de preprocesador?

Existe la GL_ARB_shading_language_includeextensión, pero tiene algunos inconvenientes:

  1. Solo es compatible con NVIDIA ( http://delphigl.de/glcapsviewer/listreports2.php?listreportsbyextension=GL_ARB_shading_language_include )
  2. Funciona especificando las cadenas de inclusión con anticipación. Por lo tanto, antes de compilar, debe especificar que la cadena "/buffers.glsl"(como se usa en #include "/buffers.glsl") corresponde al contenido del archivo buffer.glsl(que ha cargado previamente).
  3. Como habrás notado en el punto (2), tus rutas deben comenzar "/", como las rutas absolutas de estilo Linux. Esta notación generalmente no es familiar para los programadores de C, y significa que no puede especificar rutas relativas.

Un diseño común es implementar su propio #includemecanismo, pero esto puede ser complicado ya que también necesita analizar (y evaluar) otras instrucciones del preprocesador #ifpara manejar adecuadamente la compilación condicional (como los protectores de encabezado).

Si implementa el suyo propio #include, también tiene algunas libertades en cómo desea implementarlo:

  • Podrías pasar cadenas por adelantado (como GL_ARB_shading_language_include).
  • Puede especificar una devolución de llamada de inclusión (esto se hace mediante la biblioteca D3DCompiler de DirectX).
  • Puede implementar un sistema que siempre lea directamente desde el sistema de archivos, como se hace en las aplicaciones C típicas.

Como simplificación, puede insertar automáticamente protectores de encabezado para cada inclusión en su capa de preprocesamiento, para que su capa de procesador se vea así:

if (#include and not_included_yet) include_file();

(Gracias a Trent Reed por mostrarme la técnica anterior).

En conclusión , no existe una solución automática, estándar y simple. En una solución futura, podría usar alguna interfaz SPIR-V OpenGL, en cuyo caso el compilador GLSL a SPIR-V podría estar fuera de la API GL. Tener el compilador fuera del tiempo de ejecución de OpenGL simplifica enormemente la implementación de cosas como, #includeya que es un lugar más apropiado para interactuar con el sistema de archivos. Creo que el método generalizado actual es simplemente implementar un preprocesador personalizado que funcione de una manera con la que cualquier programador de C debería estar familiarizado.

Nicolas Louis Guillemot
fuente
Los sombreadores también se pueden separar en módulos usando glslify , aunque solo funciona con node.js.
Anderson Green
9

Por lo general, solo uso el hecho de que glShaderSource (...) acepta una matriz de cadenas como entrada.

Utilizo un archivo de definición de sombreador basado en json, que especifica cómo se compone un sombreador (o un programa para ser más correcto), y allí especifico que el preprocesador define que pueda necesitar, los uniformes que usa, el archivo de sombreadores de vértices / fragmentos, y todos los archivos adicionales de "dependencia". Estas son solo colecciones de funciones que se agregan a la fuente antes que la fuente del sombreador real.

Solo para agregar, AFAIK, el Unreal Engine 4 utiliza una directiva #include que se analiza y agrega todos los archivos relevantes, antes de la compilación, como sugería.

Matteo Bertello
fuente
4

No creo que haya una convención común, pero si tuviera que adivinar, diría que casi todos implementan alguna forma simple de inclusión textual como un paso de preprocesamiento (una #includeextensión), porque es muy fácil de hacer. asi que. (En JavaScript / WebGL, puede hacerlo con una expresión regular simple, por ejemplo). La ventaja de esto es que puede realizar el preprocesamiento en un paso fuera de línea para las compilaciones de "lanzamiento", cuando el código del sombreador ya no necesita ser cambiado.

De hecho, una indicación de que este enfoque es común es el hecho de que una extensión ARB se introdujo para que: GL_ARB_shading_language_include. No estoy seguro de si esto se convirtió en una característica central en este momento o no, pero la extensión se escribió en OpenGL 3.2.

glampert
fuente
2
GL_ARB_shading_language_include no es una característica central. De hecho, solo NVIDIA lo admite. ( delphigl.de/glcapsviewer/… )
Nicolas Louis Guillemot
4

Algunas personas ya han señalado que glShaderSourcepueden tomar una serie de cadenas.

Además de eso, en GLSL la compilación ( glShaderSource, glCompileShader) y el enlace ( glAttachShader, glLinkProgram) del sombreador están separados.

Lo he usado en algunos proyectos para dividir los sombreadores entre la parte específica y las partes comunes a la mayoría de los sombreadores, que luego se compila y comparte con todos los programas de sombreadores. Esto funciona y no es difícil de implementar: solo tiene que mantener una lista de dependencias.

Sin embargo, en términos de mantenibilidad, no estoy seguro de que sea una victoria. La observación fue la misma, intentemos factorizar. Si bien de hecho evita la repetición, la sobrecarga de la técnica se siente significativa. Además, el sombreador final es más difícil de extraer: no puede simplemente concatenar las fuentes del sombreador, ya que las declaraciones terminan en un orden que algunos compiladores rechazarán o se duplicarán. Por lo tanto, es más difícil hacer una prueba rápida de sombreado en una herramienta separada.

Al final, esta técnica aborda algunos problemas SECOS, pero está lejos de ser ideal.

En un tema secundario, no estoy seguro de si este enfoque tiene algún impacto en términos de tiempo de compilación; He leído que algunos controladores solo compilan realmente el programa de sombreado al vincular, pero no he medido.

Julien Guertault
fuente
Según tengo entendido, creo que esto no resuelve el problema de compartir definiciones de estructuras.
Nicolas Louis Guillemot
@NicolasLouisGuillemot: sí, tiene razón, solo el código de instrucciones se comparte de esta manera, no las declaraciones.
Julien Guertault