2012-03-26 10 views
23

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 similarmente simple) 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.Estructura del programa OpenGL y OOP

creo que tiene sentido tener diferentes clases de diferentes tipos de objetos (Rock, Tree, Character, etc), pero me pregunto cómo romper limpiamente seguridad de los datos y la prestación de la funcionalidad 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 bucle (en una clase World o Scene) que itera sobre todos los objetos de la escena y los renderiza.

¿Debería implicar llamar a un método de representación en cada objeto (Rock::render(), Tree::render(),...) o un único método de representación que adopte 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 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?

Respuesta

22

En primer lugar, ni siquiera molestarse 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 almacenamientos intermedios 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.

Te conviene 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 alguna clase de material que esté asociada al renverable.

Cada uno de los elementos que se pueden representar en una escena suele ser un componente de un nodo en un gráfico de escena jerárquico, donde cada nodo generalmente 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 scripting/behavior de la mayoría de las aplicaciones interactivas en 3D está estrechamente conectada o enganchada en su marco de nodo de gráfico de escena y un sistema de evento/mensaje.

Todo esto se combina 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 puede ser muy complejo dependiendo de qué tan generalizado y el rendimiento que desea ser y qué tipo de complejidad visual que está buscando.

Su pregunta de draw(renderable), vs renderable.draw() es más o menos irrelevante hasta que determine cómo encajan todas las partes juntas.

[Update]Después de trabajar en este espacio un poco más, una cierta penetración añadido:

Habiendo dicho que, en motores comerciales, por lo general su más como draw(renderBatch) donde cada rinden lote es una agregación de objetos que son homogéneos de alguna manera significativa para la GPU, ya que iterar sobre objetos heterogéneos (en un gráfico de escena OOP "puro" mediante polimorfismo) y llamar al 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.

+1

Gracias, eso me da algunas ideas para trabajar. Una cosa que no entiendo del todo es la distinción entre la clase que se puede hacer referencia y la clase de malla que mencionaste. ¿No querría que la clase de malla sea un renderizable que pueda dibujarse? En un nivel superior, creo que el diseño será más complicado de lo que esperaba. ¿Conoce algún recurso en línea que brinde una buena introducción al diseño de un sistema de renderizado? La mayoría de los tutoriales de OpenGL que he encontrado introducen el proceso de dibujar, texturizar e iluminar algunos triángulos, sin mucha discusión sobre la arquitectura de imágenes más grande. – Jeff

+0

@Jeff: No. Una malla es solo una colección de datos de vértices. Una malla * sola * no es suficiente. Para representar algo, necesita una malla, el sombreador que desea renderizar con ella y el otro estado variado (texturas, etc.). –

+0

@Jeff lo que dijo Nicol es correcto. No todo lo que dibujas será una malla (por ejemplo, también puedes dibujar líneas o algún otro primitivo como quads, donde una malla puede ser excesiva). Una clase de malla debe describir la geometría (y, a veces, el índice material por tri, y la agrupación/jerarquía), pero no mucho más. Mire en los libros que hablan sobre el desarrollo del motor del juego (incluso si no está haciendo un juego). Una buena es "Game Engine Architecture", también las "Gemas de programación de juegos" y "Game Engine Gems" tienen mucha información buena. Es posible que también desee acechar los foros de gamedev.net –

3

Creo que OpenSceneGraph es como una respuesta. Échale un vistazo y es implementation. Debería proporcionarle algunas ideas interesantes sobre cómo usar OpenGL, C++ y OOP.

+0

Gracias por la sugerencia; Lo verificaré y veré si puedo obtener algo de eso. Creo que lo que realmente necesito, es algo un poco más pequeño en escala, un trampolín en algún lugar entre los tutoriales típicos de OpenGL de renderizar un cubo iluminado, con textura, que gira (por ejemplo) y una biblioteca de gráficos completa. – Jeff

0

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. En primer lugar me separo la funcionalidad en clases tales como:

  • Object - recipiente que contiene toda la información necesaria objeto
  • AssetManager - cargas de los modelos y texturas, ellos (unique_ptr) posee, devuelve un puntero a la prima los recursos para el objeto
  • Renderer: maneja todas las llamadas OpenGL etc., asigna los almacenamientos intermedios en la GPU y devuelve los manejadores de representación de los recursos al objeto (cuando quiero que el renderizador dibuje el objeto que yo llamo el representador que le da el modelo render handle , identificador de textura y matriz de modelo), el renderizador debe agregar dicha información o poder dibujarlos en lotes
  • Física: cálculos que usan el objeto junto con sus recursos (especialmente vértices)
  • Escena: conecta todo lo anterior, también puede contener algunos gráficos de escenas, depende de la naturaleza de la aplicación (puede tener múltiples gráficos, BVH para colisiones, otras representaciones para la optimización del sorteo, etc.)

El problema es que la 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 representador ahora se puede transformar en algo como GPUManager y otras abstracciones encima. 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 la CPU excesiva < -> comunicación GPU.