Cómo almacenar en caché los recursos en mi sistema de renderizado casero

10

Antecedentes:

Estoy diseñando un sistema de renderizado 3D simple para una arquitectura de tipo de sistema de componente de entidad usando C ++ y OpenGL. El sistema consta de un renderizador y un gráfico de escena. Cuando termine la primera iteración del renderizador, podría distribuir el gráfico de escena en la arquitectura ECS. Por ahora es marcador de posición de una forma u otra. Si es posible, los siguientes son mis objetivos para el procesador:

  1. Simplicidad . Esto es para un proyecto de investigación y quiero poder cambiar y expandir mis sistemas fácilmente (de ahí el enfoque ECS).
  2. Rendimiento . Mi escena puede tener muchos modelos pequeños y también grandes volúmenes con mucha geometría. No es aceptable adquirir objetos del contexto OGL y la geometría del búfer en cada cuadro de renderizado. Estoy apuntando a la localidad de datos para evitar errores de caché.
  3. Flexibilidad . Debe poder representar sprites, modelos y volúmenes (vóxeles).
  4. Desacoplado . El gráfico de escena se puede refactorizar en la arquitectura central de ECS después de escribir mi renderizador.
  5. Modular . Sería bueno poder intercambiar diferentes renderizadores sin cambiar el gráfico de mi escena.
  6. Transparencia referencial , lo que significa que en cualquier momento puedo darle una escena válida y siempre se mostrará la misma imagen para esa escena. Este objetivo en particular no es necesariamente obligatorio. Pensé que ayudaría a simplificar la serialización de escenas (necesitaré poder guardar y cargar escenas) y darme flexibilidad para intercambiar diferentes escenas durante el tiempo de ejecución con fines de prueba / experimentación.

Problema e ideas:

Se me ocurrieron algunos enfoques diferentes para probar, pero estoy luchando con la forma de almacenar en caché los recursos OGL (VAO, VBO, sombreadores, etc.) para cada nodo de representación. Los siguientes son los diferentes conceptos de almacenamiento en caché que he pensado hasta ahora:

  1. Caché centralizada. Cada nodo de escena tiene una ID y el renderizador tiene un caché que asigna ID para representar los nodos. Cada nodo de representación contiene los VAO y VBO asociados con la geometría. Un error de caché adquiere recursos y asigna la geometría a un nodo de representación en el caché. Cuando se cambia la geometría, se establece una bandera sucia. Si el renderizador ve una bandera de geometría sucia mientras itera por los nodos de la escena, rechaza los datos utilizando el nodo de renderizado. Cuando se elimina un nodo de escena, se difunde un evento y el procesador elimina el nodo de procesamiento asociado de la caché mientras libera recursos. Alternativamente, el nodo está marcado para su eliminación y el procesador es responsable de eliminarlo. Creo que este enfoque logra el objetivo 6 más cercano al tiempo que considera 4 y 5. 2 sufre la complejidad adicional y la pérdida de la localidad de datos con búsquedas de mapas en lugar de acceso a la matriz.
  2. Caché distribuido . Similar arriba excepto que cada nodo de escena tiene un nodo de renderizado. Esto evita la búsqueda en el mapa. Para abordar la localidad de datos, los nodos de renderizado podrían almacenarse en el renderizador. Luego, los nodos de la escena podrían tener punteros para representar los nodos y el renderizador establece el puntero en un error de caché. Creo que este tipo de imita un enfoque de componente de entidad, por lo que sería coherente con el resto de la arquitectura. El problema aquí es que ahora los nodos de escena contienen datos específicos de implementación de renderizador. Si cambio la forma en que se representan las cosas en el renderizador (como renderizar sprites frente a volúmenes) ahora necesito cambiar el nodo de renderizado o agregar más "componentes" al nodo de escena (lo que también significa cambiar el gráfico de escena). En el lado positivo, esta parece ser la forma más simple de poner en funcionamiento mi renderizador de primera iteración.
  3. Metadatos distribuidos . Un componente de metadatos de caché de renderizador se almacena en cada nodo de escena. Estos datos no son específicos de la implementación, sino que contienen un ID, tipo y cualquier otro dato relevante que necesita la memoria caché. Luego, la búsqueda de caché se puede hacer directamente en una matriz usando la ID, y el tipo puede indicar qué tipo de enfoque de representación usar (como sprites vs volúmenes).
  4. Visitante + mapeo distribuido . El renderizador es un visitante y los nodos de escena son elementos en el patrón de visitante. Cada nodo de escena contiene una clave de caché (como los metadatos pero solo una ID) que solo el renderizador manipula. La ID se puede usar para la matriz en lugar de la búsqueda generalizada de mapas. El renderizador puede permitir que el nodo de escena distribuya una función de representación diferente según el tipo de nodo de escena, y cualquier caché puede usar la ID. Una identificación predeterminada o fuera de rango indicaría una falta de caché.

Como resolverías este problema? ¿O tienes alguna sugerencia? ¡Gracias por leer mi muro de texto!

Sean
fuente
1
¿Has hecho algún progreso?
Andreas
Esta es una pregunta extremadamente compleja, y probablemente debería dividirse en varias preguntas separadas. Esto es esencialmente preguntar "¿Cómo debo diseñar mi motor?" Mi consejo sería diseñar primero algo que admita los componentes más simples, luego agregar y refactorizar las características a medida que avanza. Además, busque libros de diseño de motores de juegos en 3D, que deberían cubrir mucha de la información que está buscando.
Ian Young

Respuestas:

2

Después de volver a leer su pregunta, siento firmemente que está complicando demasiado el problema. Este es el por qué:

  1. En realidad, solo hay dos tipos de sistemas de representación: adelante y diferido, ninguno de los cuales depende de un gráfico de escena.

  2. Los únicos problemas de rendimiento que realmente debería tener con cualquier sistema de renderizado, deberían provenir de un alto recuento de polígonos y un sombreador y código de cliente ineficientes.

  3. Los errores de caché reducen el rendimiento, pero no son los monstruos que crees que son. El 80% de las mejoras de rendimiento serán de un algoritmo más eficiente. No cometa el error de optimizar previamente su código.

Eso dijo:

Si está utilizando una escena homebrew, entonces ya debería estar usando una interfaz "Renderer" (o clase base) para diseñar la parte de representación de su código de escena. El patrón de visitante que usa el envío doble es un buen enfoque para esto, ya que bien puede estar usando muchos tipos de nodos gráficos como color, textura, malla, transformación, etc. De esta manera, durante el ciclo de renderizado, todo lo que el renderizador tiene que hacer es caminar por la estructura del árbol de la escena primero en profundidad, pasándose a sí mismo como argumento. De esta manera, el renderizador es básicamente una colección de sombreadores y quizás un framebuffer o dos. El resultado de esto es que el código de búsqueda / eliminación ya no es necesario para el sistema de renderizado, solo la escena en sí.

Ciertamente, hay otras formas de abordar los problemas que enfrenta, pero no quiero darle una respuesta sin aliento por mucho tiempo. Entonces, mi mejor consejo es hacer que algo simple funcione primero, luego expandirlo para encontrar sus debilidades, luego experimentar con otros enfoques y ver dónde residen sus fortalezas / debilidades en la práctica.

Entonces estará bien ubicado para tomar una decisión informada.

Ian Young
fuente