OpenGL ultramoderno. Parte 1



Hola a todos Todos los que conocen un poco sobre el tema de OpenGL saben que hay una gran cantidad de artículos y cursos sobre este tema, pero muchos no afectan la API moderna, y algunos de ellos generalmente hablan de glBegin y glEnd . Intentaré cubrir algunos de los matices de la nueva API a partir de la versión 4. Enlace a la segunda parte del artículo.

Esta vez intentaré escribir un artículo interesante e informativo, y lo que sucedió depende de los buenos residentes de habra para decidir. Perdóneme por mi mala gramática (agradeceré las correcciones).

Si te gusta, escribiré sobre optimizar OpenGL y reducir DrawCalls.

¡Empecemos!

Lo que habrá en este artículo: la funcionalidad de OpenGL moderno
Lo que no estará en este artículo: enfoques modernos para renderizar en OpenGL

Contenido:
  • Acceso directo al estado
  • Depurar
  • Objetos de sombreador separados
  • Matrices de texturas
  • Vista de textura
  • Búfer único para índice y vértice
  • Teselación y sombreado de cálculo
  • Representación de ruta


DSA (acceso directo al estado)


Acceso directo al estado: acceso directo al estado. Una herramienta para modificar objetos OpenGL sin tener que ajustarlos al contexto. Esto le permite cambiar el estado de un objeto en un contexto local sin afectar el estado global compartido por todas las partes de la aplicación. También hace que la API esté un poco más orientada a objetos, ya que las funciones que cambian el estado de los objetos se pueden definir claramente. Esto es lo que nos dice el OpenGL Wiki .

Como sabemos, OpenGL es una API con muchos botones de opción : glActiveTexture , glBindTexture , etc.

A partir de aquí tenemos algunos problemas :

  • El selector y los estados actuales pueden hacer un cambio más profundo en el estado.
  • Puede ser necesario vincular / cambiar la unidad activa para establecer un filtro para texturas
  • La gestión del estado se vuelve problemática como resultado de lo cual crece la complejidad de la aplicación
  • El estado desconocido conduce a configuraciones adicionales
  • Los intentos de guardar / restaurar el estado pueden ser problemáticos

¿Qué nos ofreció el grupo Khronos y cómo ayuda el DSA?

  • Agrega funciones que funcionan directamente con el objeto / objetos
  • Establece el filtro de textura para el objeto de textura especificado, no el actual.
  • Vincula una textura a una unidad específica, no a la activa
  • Agrega una gran cantidad de nuevas características
  • Cubre cosas hasta OpenGL 1.x
  • Agrega características adicionales.

En teoría, un DSA puede ayudar a reducir el número de operaciones que no cambian de estado y de cambio a cero ... Pero eso no es exacto.

Ahora repasaré brevemente algunas de las nuevas funciones, no me detendré en los parámetros en detalle, dejaré enlaces en la wiki.

  • glCreateTextures reemplaza glGenTextures + glBindTexture (inicialización).
    Fue:
    glGenTextures(1, &name); glBindTexture(GL_TEXTURE_2D, name); 
    Se convirtió en:
     glCreateTextures(GL_TEXTURE_2D, 1, &name); 
  • glTextureParameterX equivalente a glTexParameterX
     glGenTextures(1, &name); glBindTexture(GL_TEXTURE_2D, name); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width, height); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels); 
    Ahora lo escribiremos así:
     glCreateTextures(GL_TEXTURE_2D, 1, &name); glTextureParameteri(name, GL_TEXTURE_WRAP_S, GL_CLAMP); glTextureParameteri(name, GL_TEXTURE_WRAP_T, GL_CLAMP); glTextureParameteri(name, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTextureParameteri(name, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTextureStorage2D(name, 1, GL_RGBA8, width, height); glTextureSubImage2D(name, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels); 
  • glBindTextureUnit reemplaza glActiveTexture + glBindTexture
    Así es como lo hicimos:
     glActiveTexture(GL_TEXTURE0 + 3); glBindTexture(GL_TEXTURE_2D, name); 
    Ahora:
     glBindTextureUnit(3, name); 

Los cambios también afectaron glTextureImage , ya no se usa, y he aquí por qué:

glTexImage es bastante inseguro, es muy fácil obtener texturas no válidas, porque la función no verifica los valores cuando se llama, el controlador hace esto mientras dibuja. GlTexStorage fue agregado para reemplazarlo .

glTexStorage proporciona una forma de crear texturas con comprobaciones realizadas durante una llamada, lo que minimiza los errores. El repositorio de texturas es resuelto por la mayoría, si no todos, los problemas causados ​​por las texturas mutables, aunque las texturas inmutables son más confiables.

Los cambios también afectaron el búfer de trama:


Estas no son todas las características modificadas. Las siguientes en la línea son las funciones para los buffers:


Aquí hay una lista de lo que ahora se incluye en el soporte DSA:

  • Objetos de matriz de vértices
  • Objetos Framebuffer
  • Programar objetos
  • Objetos de amortiguación
  • Pilas de matriz
  • Muchas cosas obsoletas

Depurar


Desde la versión 4.3, se ha agregado una nueva funcionalidad para la depuración, en mi opinión, muy útil y conveniente. Ahora OpenGL llamará a nuestra devolución de llamada por errores y mensajes de depuración, cuyo nivel podemos ajustar.



Necesitamos invocar solo dos funciones para habilitar: glEnable y glDebugMessageCallback , en ningún lugar es más fácil.

 glEnable(GL_DEBUG_OUTPUT); glDebugMessageCallback(message_callback, nullptr); 

Ahora escribiremos una función de devolución de llamada para obtener el mensaje:

 void callback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, GLchar const* message, void const* user_param) { auto source_str = [source]() -> std::string { switch (source) { case GL_DEBUG_SOURCE_API: return "API"; case GL_DEBUG_SOURCE_WINDOW_SYSTEM: return "WINDOW SYSTEM"; case GL_DEBUG_SOURCE_SHADER_COMPILER: return "SHADER COMPILER"; case GL_DEBUG_SOURCE_THIRD_PARTY: return "THIRD PARTY"; case GL_DEBUG_SOURCE_APPLICATION: return "APPLICATION"; case GL_DEBUG_SOURCE_OTHER: return "OTHER"; default: return "UNKNOWN"; } }(); auto type_str = [type]() { switch (type) { case GL_DEBUG_TYPE_ERROR: return "ERROR"; case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: return "DEPRECATED_BEHAVIOR"; case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: return "UNDEFINED_BEHAVIOR"; case GL_DEBUG_TYPE_PORTABILITY: return "PORTABILITY"; case GL_DEBUG_TYPE_PERFORMANCE: return "PERFORMANCE"; case GL_DEBUG_TYPE_MARKER: return "MARKER"; case GL_DEBUG_TYPE_OTHER: return "OTHER"; default: return "UNKNOWN"; } }(); auto severity_str = [severity]() { switch (severity) { case GL_DEBUG_SEVERITY_NOTIFICATION: return "NOTIFICATION"; case GL_DEBUG_SEVERITY_LOW: return "LOW"; case GL_DEBUG_SEVERITY_MEDIUM: return "MEDIUM"; case GL_DEBUG_SEVERITY_HIGH: return "HIGH"; default: return "UNKNOWN"; } }(); std::cout << source_str << ", " << type_str << ", " << severity_str << ", " << id << ": " << message << std::endl; } 

También podemos configurar el filtro usando glDebugMessageControl . El filtro puede funcionar en modo de filtrado por fuente / tipo / importancia o un conjunto de mensajes utilizando sus identificadores.

Filtrar mensajes en un ámbito específico:

 glPushDebugGroup( GL_DEBUG_SOURCE_APPLICATION, DEPTH_FILL_ID, 11, “Depth Fill”); //  Render_Depth_Only_Pass(); //  glPopDebugGroup(); //  

Será muy útil deshabilitar la llamada asincrónica para que podamos determinar el orden de las llamadas a funciones, así como encontrar el lugar del error en la pila al depurar. Esto se hace simplemente:

 glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); 

Vale la pena recordar que no es seguro llamar a OpenGL o funciones de ventana en una función de llamada envuelta, así como las devoluciones de llamada no son gratuitas y no debe dejarlas incluidas en la versión de lanzamiento.
Más detalles sobre estas y otras pequeñas cosas: trucos, puedes leer aquí .

SSO (objetos de sombreado separados)


Una vez que OpenGL funcionaba como una "tubería fija", esto significaba que el procesamiento preprogramado se aplicaba a todos los datos transferidos para su visualización. El siguiente paso fue la "tubería programable", donde la parte programable implementa sombreadores, escrito en GLSL, el programa GLSL clásico consistía en sombreadores de vértices y fragmentos, pero en OpenGL moderno se agregaron algunos tipos nuevos de sombreadores, a saber, los sombreadores de geometría, ponderación y cálculos (sobre ellos I Lo contaré en la siguiente parte).


Los SSO nos permiten cambiar los pasos del sombreador sobre la marcha sin volver a vincularlos. La creación y configuración de una tubería de software simple sin depuración es la siguiente:

 GLuint pipe = GL_NONE; // Create shaders GLuint fprog = glCreateShaderProgramv( GL_FRAGMENT_SHADER, 1, &text); GLuint vprog = glCreateShaderProgramv( GL_VERTEX_SHADER, 1, &text); // Bind pipeline glGenProgramPipelines( 1, &pipe); glBindProgramPipelines( pipe); // Bind shaders glUseProgramStages( pipe, GL_FRAGMENT_SHADER_BIT, fprog); glUseProgramStages( pipe, GL_VERTEX_SHADER_BIT, vprog); 

Como vemos, glCreateProgramPipelines genera un descriptor e inicializa el objeto, glCreateShaderProgramv genera, inicializa, compila y vincula el programa de sombreado utilizando las fuentes especificadas, y glUseProgramStages adjunta los pasos del programa al objeto de canalización. glBindProgramPipeline : asocia una canalización con un contexto.

Pero hay una advertencia, ahora los parámetros de entrada y salida de los sombreadores deben coincidir. Podemos declarar los parámetros de entrada / salida en el mismo orden, con los mismos nombres, o hacemos que su ubicación coincida claramente con la ayuda de calificadores.
Recomiendo la última opción, esto nos permitirá configurar una interfaz claramente definida, así como ser flexible con respecto a los nombres y el orden.

Para proporcionar una interfaz más rigurosa, también necesitamos declarar los bloques de entrada y salida incorporados que queremos usar para cada etapa.

Las interfaces de bloque incorporadas se definen como ( de una wiki ):
Vértice:

 out gl_PerVertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; }; 

Control de teselación:
 out gl_PerVertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; } gl_out[]; 

Evaluación de teselación:
 out gl_PerVertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; }; 

Geometría:
 out gl_PerVertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; }; 

Un ejemplo de volver a declarar un módulo integrado y usar la ubicación de atributos en un sombreador de vértices regular:

 #version 450 out gl_PerVertex { vec4 gl_Position; }; layout (location = 0) in vec3 position; layout (location = 1) in vec3 color; layout (location = 0) out v_out { vec3 color; } v_out; void main() { v_out.color = color; gl_Position = vec4(position, 1.0); } 

Source: https://habr.com/ru/post/456932/


All Articles