c++ - original - Estructura del programa OpenGL y OOP
opengl standard (3)
He trabajado en una variedad de proyectos de demostración con OpenGL y C ++, pero todos han implicado simplemente renderizar un solo cubo (o malla similar) con algunos efectos interesantes. Para una escena simple como esta, los datos de vértice para el cubo podrían almacenarse en una matriz global poco elegante. Ahora estoy buscando renderizar escenas más complejas, con múltiples objetos de diferentes tipos.
Creo que tiene sentido tener diferentes clases para diferentes tipos de objetos ( Rock
, Tree
, Character
, etc.), pero me pregunto cómo dividir limpiamente la funcionalidad de datos y renderización de los objetos en la escena. Cada clase almacenará su propia matriz de posiciones de vértices, coordenadas de textura, normales, etc. Sin embargo, no estoy seguro de dónde colocar las llamadas de OpenGL. Estoy pensando que tendré un loop (en una clase World
o Scene
) que iterará sobre todos los objetos en la escena y los renderizará.
En caso de representarlos, llame a un método de renderizado en cada objeto (Rock::render(), Tree::render(),...)
o un único método de renderizado que tome un objeto como parámetro (render(Rock), render(Tree),...)
? Este último parece más limpio, ya que no tendré código duplicado en cada clase (aunque eso podría mitigarse heredando de una sola clase RenderableObject
), y permite que el método render () sea fácilmente reemplazado si deseo hacer un puerto posterior a DirectX. Por otro lado, no estoy seguro si puedo mantenerlos separados, ya que podría necesitar tipos específicos de OpenGL almacenados en los objetos de todos modos (búferes de vértices, por ejemplo). Además, parece un poco engorroso tener la funcionalidad de renderización separada del objeto, ya que tendrá que llamar a muchos métodos de Get()
para obtener los datos de los objetos. Finalmente, no estoy seguro de cómo manejaría este sistema los objetos que tienen que dibujarse de diferentes maneras (diferentes sombreadores, diferentes variables para pasar a los sombreadores, etc.).
¿Es uno de estos diseños claramente mejor que el otro? ¿De qué maneras puedo mejorarlos para mantener mi código bien organizado y eficiente?
Creo que OpenSceneGraph es una especie de respuesta. Échale un vistazo y su implementation . Debería proporcionarle algunas ideas interesantes sobre cómo usar OpenGL, C ++ y OOP.
En primer lugar, ni siquiera te molestes con la independencia de la plataforma en este momento. espera hasta que tengas una idea mucho mejor de tu arquitectura.
Hacer muchas llamadas de sorteo / cambios de estado es lento. La forma en que lo haces en un motor es que generalmente querrás tener una clase renderizable que pueda dibujarse a sí misma. Este renderizable se asociará a los búferes que necesite (por ejemplo, búferes de vértices) y otra información (como formato de vértice, topología, búferes de índice, etc.). Los diseños de entrada Shader se pueden asociar a formatos de vértices.
Querrá tener algunas clases de geo primitivas, pero diferir cualquier cosa compleja para algún tipo de clase de malla que maneje tris indexados. Para una aplicación de rendimiento, querrá agrupar las llamadas (y potencialmente los datos) para tipos de entrada similares en su canal de sombreado para minimizar los cambios de estado innecesarios y los vaciados de tuberías.
Los parámetros y las texturas de los sombreadores generalmente se controlan a través de una clase de material que está asociada a los rendables.
Cada representable en una escena en sí es generalmente un componente de un nodo en un gráfico de escena jerárquica, donde cada nodo usualmente hereda la transformación de sus antepasados a través de algún mecanismo. Es probable que desee un ejecutante de escena que utilice un esquema de partición espacial para hacer una determinación rápida de la visibilidad y evitar la sobrecarga de llamar para que las cosas no se vean.
La parte de secuencias de comandos / comportamiento de la mayoría de las aplicaciones interactivas en 3D está estrechamente conectada o enganchada a su marco de nodo de gráfico de escena y un sistema de evento / mensaje.
Todo esto encaja en un bucle de alto nivel donde actualiza cada subsistema en función del tiempo y dibuja la escena en el fotograma actual.
Obviamente, hay toneladas de pequeños detalles omitidos, pero pueden ser muy complejos según lo generalizado y el rendimiento que desee y el tipo de complejidad visual que busca.
Su pregunta de draw(renderable)
, vs renderable.draw()
es más o menos irrelevante hasta que determine cómo encajan todas las partes.
[Actualización] Después de trabajar en este espacio un poco más, algo de información adicional :
Habiendo dicho eso, en motores comerciales, usualmente es más como draw(renderBatch)
donde cada lote render es una agregación de objetos que son homogéneos de alguna manera significativa para la GPU, ya que iteran sobre objetos heterogéneos (en un gráfico de escena OOP "puro" a través del polimorfismo) y llamar a obj.draw()
uno por uno tiene una localidad de memoria caché horrible y generalmente es un uso ineficiente de los recursos de la GPU. Es muy útil adoptar un enfoque orientado a los datos para diseñar la forma en que un motor habla con sus API de gráficos subyacentes de la manera más eficiente posible, agrupando todo lo posible sin afectar negativamente la estructura / legibilidad del código.
Una sugerencia práctica es escribir un primer motor usando un enfoque ingenuo / "puro" para familiarizarse realmente con el espacio del dominio. Luego, en una segunda pasada (o probablemente reescriba), concéntrese en el hardware: cosas como representación de memoria, localidad de caché, estado de tubería, ancho de banda, procesamiento por lotes y paralelismo. Una vez que realmente empiezas a considerar estas cosas, te darás cuenta de que la mayor parte de tu diseño inicial se va por la ventana. Buena diversión.
Esto es lo que he implementado para una simulación física y lo que funcionó bastante bien y estaba en un buen nivel de abstracción. Primero separaría la funcionalidad en clases tales como:
- Objeto: contenedor que contiene toda la información de objeto necesaria
- AssetManager: carga los modelos y las texturas, los posee (unique_ptr), devuelve un puntero sin formato a los recursos del objeto
- Renderer: maneja todas las llamadas OpenGL etc., asigna los buffers en GPU y devuelve los manejadores de render de los recursos al objeto (cuando quiere que el renderizador dibuje el objeto que llamo el renderizador, le da el manejador de renderizado, el manejador de texturas y la matriz del modelo) el renderizador debe agregar dicha información o ser capaz de dibujarlas en lotes
- Física: cálculos que usan el objeto junto con sus recursos (especialmente los vértices)
- Escena: conecta todo lo anterior, también puede contener algún gráfico de escena, depende de la naturaleza de la aplicación (puede tener múltiples gráficos, BVH para colisiones, otras representaciones para optimizar el sorteo, etc.)
El problema es que GPU ahora es GPGPU (GPU de propósito general) por lo que OpenGL o Vulkan ya no es solo un framework de renderizado. Por ejemplo, se realizan cálculos físicos en la GPU. Por lo tanto, el renderizador puede ahora transformarse en algo así como GPUManager y otras abstracciones sobre él. También la forma más óptima de dibujar es en una llamada. En otras palabras, un gran búfer para toda la escena que también se puede editar a través de sombreadores de cómputo para evitar una excesiva comunicación CPU <-> GPU.