Estoy escribiendo un juego usando C ++ y OpenGL 2.1. Estaba pensando cómo podría separar los datos / la lógica del renderizado. Por el momento utilizo una clase base 'Renderable' que proporciona un método virtual puro para implementar el dibujo. Pero cada objeto tiene un código tan especializado que solo el objeto sabe cómo configurar correctamente los uniformes de sombreador y organizar los datos del búfer de la matriz de vértices. Termino con muchas llamadas de función gl * en todo mi código. ¿Hay alguna forma genérica de dibujar los objetos?
21
m_renderable
miembro. De esa manera, puedes separar mejor tu lógica. No aplique la "interfaz" renderizable en objetos generales que también tengan física, ai y otras cosas. Después de eso, puede administrar los renderizables por separado. Necesita una capa de abstracción sobre las llamadas a la función OpenGL para desacoplar las cosas aún más. Por lo tanto, no espere que un buen motor tenga llamadas GL API dentro de sus diversas implementaciones renderizables. Eso es todo, en pocas palabras.Respuestas:
Una idea es utilizar el patrón de diseño Visitante. Necesita una implementación de Renderer que sepa cómo renderizar accesorios. Cada objeto puede llamar a la instancia del renderizador para manejar el trabajo de renderizado.
En unas pocas líneas de pseudocódigo:
El material gl * se implementa mediante los métodos del renderizador, y los objetos solo almacenan los datos necesarios para renderizar, posición, tipo de textura, tamaño ... etc.
Además, puede configurar diferentes renderizadores (debugRenderer, hqRenderer, ... etc.) y usarlos dinámicamente, sin cambiar los objetos.
Esto también se puede combinar fácilmente con los sistemas de entidad / componente.
fuente
Entity/Component
un poco más la alternativa, ya que puede ayudar a separar a los proveedores de geometría de otras partes del motor (IA, física, redes o juego en general). +1!ObjectA
yObjectB
porDrawableComponentA
yDrawableComponentB
, y dentro de los métodos de renderizado, usar otros componentes si lo necesita, como:position = component->getComponent("Position");
Y en el bucle principal, tiene una lista de componentes dibujables para llamar a draw.Renderable
) que tiene unadraw(Renderer&)
función y todos los objetos que se pueden representar los implementan? ¿En qué casoRenderer
solo necesita una función que acepte cualquier objeto que implemente la interfaz común y la llamadarenderable.draw(*this);
?gl_*
funciones al renderizador (separando la lógica del renderizado), pero su solución mueve lasgl_*
llamadas a los objetos.Sé que ya has aceptado la respuesta de Zhen, pero me gustaría publicar otra en caso de que ayude a alguien más.
Para reiterar el problema, el OP quiere la capacidad de mantener el código de representación separado de la lógica y los datos.
Mi solución es usar una clase diferente todos juntos para representar el componente, que es independiente de la
Renderer
clase y la lógica. Primero debe haber unaRenderable
interfaz que tenga una funciónbool render(Renderer& renderer);
y laRenderer
clase usa el patrón de visitante para recuperar todas lasRenderable
instancias, dada la lista deGameObject
sy representa los objetos que tienen unaRenderable
instancia. De esta manera, Renderer no necesita saber de cada tipo de objeto y sigue siendo responsabilidad de cada tipo de objeto informarlo aRenderable
través de lagetRenderable()
función. O bien, puede crear unaRenderableVisitor
clase que visite todos los GameObjects y, en función de laGameObject
condición individual , pueden elegir agregar / no agregar su representación al visitante. De cualquier manera, la esencia principal es que elgl_*
todas las llamadas están fuera del objeto en sí y residen en una clase que conoce detalles íntimos del objeto en sí, en lugar de ser parte de esoRenderer
.DESCARGO DE RESPONSABILIDAD : escribí a mano estas clases en el editor, por lo que hay una buena posibilidad de que me haya perdido algo en el código, pero con suerte, entenderás la idea.
Para mostrar un ejemplo (parcial):
Renderable
interfazGameObject
clase:(Parcial)
Renderer
clase.RenderableObject
clase:ObjectA
clase:ObjectARenderable
clase:fuente
Construye un sistema de renderizado-comando. Un objeto de alto nivel, que tiene acceso tanto a
OpenGLRenderer
la escena como a los objetos de juego / gráfico, iterará el gráfico de escena o los objetos de juego y creará un lote deRenderCmds
, que luego se enviará alOpenGLRenderer
que dibujará cada uno a su vez y, por lo tanto, contendrá todo OpenGL código relacionado en el mismo.Hay más ventajas en esto que solo la abstracción; eventualmente, a medida que crece su complejidad de renderizado, puede ordenar y agrupar cada comando de renderizado por textura o sombreador, por ejemplo,
Render()
para eliminar muchos cuellos de botella en las llamadas de dibujo que pueden marcar una gran diferencia en el rendimiento.fuente
Depende completamente de si puedes hacer suposiciones sobre lo que es común para todas las entidades renderizables o no. En mi motor, todos los objetos se representan de la misma manera, por lo que solo necesito proporcionar vbos, texturas y transformaciones. Luego, el renderizador los busca a todos, por lo que no se necesitan llamadas a funciones OpenGL en los diferentes objetos.
fuente
Definitivamente ponga el código de representación y la lógica del juego en diferentes clases. La composición (como sugirió el teodron) es probablemente la mejor manera de hacer esto; Cada entidad en el mundo del juego tendrá su propio Renderable, o tal vez un conjunto de ellos.
Es posible que aún tenga varias subclases de Renderable, por ejemplo, para manejar animación esquelética, emisores de partículas y sombreadores complejos, además de su sombreador básico con textura e iluminación. La clase Renderable y sus subclases solo deben contener la información necesaria para el renderizado: geometría, texturas y sombreadores.
Además, debe separar una instancia de una malla dada de la malla misma. Digamos que tienes cien árboles en la pantalla, cada uno con la misma malla. Solo desea almacenar la geometría una vez, pero necesitará matrices separadas de ubicación y rotación para cada árbol. Los objetos más complejos, como los humanoides animados, también tendrán información de estado adicional (como un esqueleto, el conjunto de animaciones aplicadas actualmente, etc.).
Para renderizar, el enfoque ingenuo es iterar sobre cada entidad del juego y decirle que se procese solo. Alternativamente, cada entidad (cuando se genera) puede insertar sus objetos renderizables en un objeto de escena. Luego, su función de renderizado le dice a la escena que renderice. Esto permite que la escena haga cosas complejas relacionadas con el renderizado sin incrustar ese código en entidades del juego o en una subclase renderizable específica.
fuente
Este consejo no es realmente específico para el renderizado, pero debería ayudar a crear un sistema que mantenga las cosas en gran medida separadas. En primer lugar, intente mantener los datos de 'GameObject' separados de la información de posición.
Vale la pena señalar que la simple información posicional de XYZ podría no ser tan simple. Si está utilizando un motor de física, los datos de posición podrían almacenarse en el motor de terceros. Debería sincronizar entre ellos (lo que implicaría una gran cantidad de copia de memoria sin sentido) o consultar la información directamente desde el motor. Pero no todos los objetos necesitan física, algunos se fijarán en su lugar, por lo que un conjunto simple de flotadores funciona bien allí. Algunos incluso pueden estar unidos a otros objetos, por lo que su posición es en realidad un desplazamiento de otra posición. En una configuración avanzada, es posible que tenga una posición almacenada solo en la GPU, la única vez que se necesitaría el lado de la computadora es para secuencias de comandos, almacenamiento y replicación de red. Por lo tanto, es probable que tenga varias opciones posibles para sus datos posicionales. Aquí tiene sentido usar la herencia.
En lugar de un objeto que posee su posición, ese objeto debería ser propiedad de una estructura de datos de indexación. Por ejemplo, un 'Nivel' podría tener un Octree, o tal vez una 'escena' de motor de física. Cuando desea renderizar (o configurar una escena de renderizado), consulta su estructura especial para los objetos que son visibles para la cámara.
Esto también ayuda a administrar bien la memoria. De esta manera, un objeto que no está realmente en un área ni siquiera tiene una posición que tenga sentido en lugar de devolver 0.0 coords o los coords que tenía cuando era el último en un área.
Si ya no mantiene las coordenadas en el objeto, en lugar de object.getX () terminaría teniendo level.getX (objeto). El problema con eso es buscar el objeto en el nivel probablemente será una operación lenta ya que tendrá que mirar a través de todos sus objetos y coincidir con el que está consultando.
Para evitar eso, probablemente crearía una clase especial de 'enlace'. Uno que se une entre un nivel y un objeto. Yo lo llamo una "ubicación". Esto contendría las coordenadas xyz, así como el controlador del nivel y el controlador del objeto. Esta clase de enlace se almacenaría en la estructura / nivel espacial y el objeto tendría una referencia débil (si el nivel / ubicación se destruye, la referencia de los objetos debe actualizarse a nulo). También podría valer la pena tener la clase Ubicación 'posee' el objeto, de esa manera si se elimina un nivel, también lo es la estructura de índice especial, las ubicaciones que contiene y sus objetos.
Ahora la información de posición se almacena solo en un lugar. No se duplica entre el objeto, la estructura de indexación espacial, el renderizador, etc.
Las estructuras de datos espaciales como los octrees a menudo ni siquiera necesitan tener las coordenadas de los objetos que almacenan. Su posición se almacena en la ubicación relativa de los nodos en la estructura misma (podría considerarse como una especie de compresión con pérdida, sacrificando la precisión para tiempos de búsqueda rápidos). Con el objeto de ubicación en Octree, las coordenadas reales se encuentran dentro de él una vez que se realiza la consulta.
O si está utilizando un motor de física para administrar las ubicaciones de sus objetos o una mezcla entre los dos, la clase Ubicación debe manejarlo de manera transparente mientras mantiene todo su código en un solo lugar.
Otra ventaja es que ahora la posición y la referencia al nivel se almacenan en la misma ubicación. Puede implementar object.TeleportTo (other_object) y hacer que funcione en todos los niveles. Del mismo modo, la búsqueda de rutas de IA podría seguir algo en un área diferente.
Con respecto a la representación. Su render puede tener un enlace similar a la ubicación. Excepto que tendría el material específico de representación allí. Probablemente no necesite el 'Objeto' o 'Nivel' para ser almacenado en esta estructura. El Objeto podría ser útil si está tratando de hacer algo como elegir un color o representar una barra de golpe flotando sobre él, etc., pero de lo contrario, el renderizador solo se preocupa por la malla y demás. RenderableStuff sería una malla, también podría tener cuadros delimitadores, etc.
Es posible que no necesite hacer esto cada fotograma, puede asegurarse de tomar una región más grande que la que la cámara muestra actualmente. Cachéalo, rastrea los movimientos de objetos para ver si hay un cuadro delimitador dentro del rango, rastrea el movimiento de la cámara, etc. Pero no comiences a jugar con ese tipo de cosas hasta que lo hayas evaluado.
El motor de física en sí podría tener una abstracción similar, ya que tampoco necesita los datos del Objeto, solo la malla de colisión y las propiedades físicas.
Todos los datos del objeto central que contendrían serían el nombre de la malla que usa el objeto. El motor del juego puede continuar y cargar esto en el formato que desee sin cargar su clase de objeto con un montón de cosas específicas de renderizado (que pueden ser específicas de su API de renderizado, es decir, DirectX vs OpenGL).
También mantiene diferentes componentes separados. Esto hace que sea fácil hacer cosas como reemplazar su motor de física, ya que esas cosas son en su mayoría autónomas en una ubicación. También hace que las pruebas unitarias sean mucho más fáciles. Puede probar cosas como consultas de física sin tener que configurar ningún objeto falso real, ya que todo lo que necesita es la clase Ubicación. También puedes optimizar las cosas más fácilmente. Esto hace que sea más obvio qué consultas debe realizar en qué clases y ubicaciones individuales para optimizarlas (por ejemplo, el nivel anterior. GetVisibleObject sería donde podría almacenar en caché las cosas si la cámara no se mueve demasiado).
fuente