Rodando mi propio gráfico de escena

23

Hola Desarrollo de juegos SE!

Me estoy arrastrando a través de OpenGL con la esperanza de crear un motor de juego simple y muy liviano. Veo el proyecto como una experiencia de aprendizaje que al final puede hacer un poco de dinero, pero será divertido de cualquier manera.

Hasta ahora he usado GLFW para obtener algunas E / S básicas, una ventana (con una tecla de pantalla completa F11 tan sofisticada) y, por supuesto, un contexto OpenGL. También he usado GLEW para exponer el resto de las extensiones de OpenGL porque estoy usando Windows y quiero usar todo OpenGL 3.0+.

Lo que me lleva al gráfico de la escena. En resumen, me gustaría rodar el mío. Esta decisión se produjo después de mirar OSG y leer algunos artículos sobre cómo el concepto de un gráfico de escena se ha torcido, doblado y roto. Uno de esos artículos describe cómo se han desarrollado los gráficos de escenas como ...

Luego agregamos todas estas cosas adicionales, como adornos colgantes en un árbol de Navidad, excepto que algunos de los adornos son deliciosos filetes jugosos y algunos son vacas vivas enteras.

Siguiendo la analogía, me gustaría el bistec, la carne de lo que debería ser un gráfico de escena, sin tener que amarrar montones de código adicional, o vacas enteras.

Entonces, con eso en mente, me pregunto exactamente qué debería ser un gráfico de escena y cómo debería implementarse un gráfico de escena simple. Esto es lo que tengo hasta ahora ...

Un padre, un árbol n-children o DAG que ...

  • Debe realizar un seguimiento de las transformaciones de los objetos del juego (posición, rotación, escala)
  • Debe contener estados de render para optimizaciones
  • Debe proporcionar un medio para eliminar objetos que no estén dentro de la vista

Con las siguientes propiedades ...

  • Todos los nodos deben tratarse como renderizables (incluso si no se procesan) Esto significa que ...

    • Todos deberían tener los métodos cull (), state () y draw () (devuelve 0 si no está visible)
    • cull () llama recursivamente a cull () en todos los hijos, generando así una malla de sacrificio completa para todo el nodo y todos los hijos. Otro método, hasChanged () podría permitir que las llamadas mallas estáticas no necesiten que su geometría de eliminación calcule cada cuadro. Esto funcionaría de manera tal que si algún nodo en el subárbol ha cambiado, se reconstruya toda la geometría hasta la raíz.
  • Los estados de representación se mantendrán en una enumeración simple, cada nodo seleccionará de esta enumeración un conjunto de estados de OpenGL que requiera y ese estado se configurará antes de que se llame a draw () en ese nodo. Esto permite el procesamiento por lotes, todos los nodos de un conjunto de estados determinado se procesarán juntos, luego se configurará el siguiente conjunto de estados y así sucesivamente.

  • Ningún nodo debe contener directamente datos de geometría / sombreado / textura, en su lugar, los nodos deben apuntar a objetos compartidos (quizás administrados por algún objeto único como un administrador de recursos).

  • Los gráficos de escena deberían poder hacer referencia a otros gráficos de escena (tal vez usando un nodo proxy) para permitir situaciones como esta , permitiendo así que se copien modelos / objetos complejos de malla múltiple alrededor del gráfico de escena sin agregar una tonelada de datos.

Espero obtener comentarios valiosos sobre mi diseño actual. ¿Le falta funcionalidad? ¿Existe una forma / patrón de diseño mucho mejor? ¿Me estoy perdiendo algún concepto más amplio que será necesario incluir en este diseño para un juego 3D algo simple? Etc.

Gracias -Cody

Cody Smith
fuente

Respuestas:

15

El concepto

Fundamentalmente, un gráfico de escena no es más que un gráfico acíclico bidireccional que sirve para representar un conjunto jerárquicamente estructurado de relaciones espaciales.

Los motores en la naturaleza tienden a incluir otras cosas buenas en el gráfico de la escena, como se señaló. Si lo ves como la carne o la vaca probablemente depende de tu experiencia con motores y bibliotecas.

Manteniéndolo ligero

Estoy a favor del estilo Unity3D de tener su nodo de gráfico de escena (que en esencia es una estructura topológica en lugar de una estructura espacial / topográfica) inherentemente incluye parámetros espaciales y funcionalidad. En mi motor, mis nodos son incluso más livianos que Unity3D, donde heredan muchos miembros basura innecesarios de superclases / interfaces implementadas: esto es lo que tengo, lo más ligero que puedes obtener:

  • miembros punteros padre / hijo.
  • miembros de parámetros espaciales pretransformados: posición xyz, cabeceo, guiñada y balanceo.
  • una matriz de transformación; las matrices en una cadena jerárquica pueden multiplicarse rápida y fácilmente caminando recursivamente hacia arriba / abajo del árbol, brindándole las transformaciones espaciales jerárquicas que son la característica principal de un gráfico de escena;
  • un updateLocal()método que actualiza solo las matrices de transformación de este nodo
  • un updateAll()método que actualiza esta y todas las matrices de transformación de nodos descendientes

... También incluyo la lógica de ecuaciones de movimiento y, por lo tanto, miembros de velocidad / aceleración (lineal y angular) en mi clase de nodo. Puede renunciar a eso y manejarlo en su controlador principal si lo desea. Pero eso es todo, muy ligero. Recuerde, podría tenerlos en miles de entidades. Entonces, como has sugerido, mantenlo ligero.

Construyendo Jerarquías

Lo que dices sobre un gráfico de escena que hace referencia a otros gráficos de escena ... ¿Estoy esperando el remate? Por supuesto que lo hacen. Ese es su uso principal. Puede agregar cualquier nodo a cualquier otro nodo, y las transformaciones deben ocurrir automáticamente dentro del espacio local de la nueva transformación. Todo lo que está haciendo es cambiar un puntero, ¡no es como si estuviera copiando datos! Al cambiar un puntero, tiene un gráfico de escena más profundo. Si usar Proxies hace las cosas más eficientes, entonces, por supuesto, pero nunca he visto la necesidad.

Evita la lógica relacionada con el renderizado

Olvídate de renderizar mientras escribes tu clase de nodo de gráfico de escena, o confundirás las cosas por ti mismo. Lo único que importa es que tenga un modelo de datos, ya sea que sea el gráfico de escena o no, y que algún renderizador inspeccionará ese modelo de datos y renderizará los objetos en el mundo en consecuencia, ya sea en 1, 2 , 3 o 7 dimensiones. El punto que estoy haciendo es: No contamine su gráfico de escena con la lógica de renderizado. Un gráfico de escena trata sobre topología y topografía, es decir, conectividad y características espaciales. Este es el verdadero estado de la simulación y existe incluso en ausencia de renderizado (que puede tomar cualquier forma bajo el sol desde una vista en primera persona a un gráfico estadístico a una descripción textual). Los nodos no apuntan a objetos relacionados con la representación; sin embargo, lo contrario puede ser cierto. Considere también esto: No todos los nodos de gráficos de escena en todo su árbol serán renderizables. Muchos serán solo contenedores. Entonces, ¿por qué incluso asignar memoria para un puntero a objeto de renderizado? Incluso un miembro de puntero que nunca se usa, todavía está ocupando memoria. Por lo tanto, invierta la dirección del puntero: la instancia relacionada con el renderizado hace referencia al modelo de datos (que podría ser, o incluir, su nodo de gráfico de escena), NO viceversa. Y si desea una manera fácil de ejecutar su lista de controladores y obtener acceso a la vista relacionada, utilice un diccionario / tabla hash, que se acerca al tiempo de acceso de lectura O (1). De esa manera no hay contaminación, y su lógica de simulación no le importa qué procesadores están en su lugar, lo que hace que sus días y noches de codificación Entonces, ¿por qué incluso asignar memoria para un puntero a objeto de renderizado? Incluso un miembro de puntero que nunca se usa, todavía está ocupando memoria. Por lo tanto, invierta la dirección del puntero: la instancia relacionada con el renderizado hace referencia al modelo de datos (que podría ser, o incluir, su nodo de gráfico de escena), NO viceversa. Y si desea una manera fácil de ejecutar su lista de controladores y obtener acceso a la vista relacionada, utilice un diccionario / tabla hash, que se acerca al tiempo de acceso de lectura O (1). De esa manera no hay contaminación, y su lógica de simulación no le importa qué procesadores están en su lugar, lo que hace que sus días y noches de codificación Entonces, ¿por qué incluso asignar memoria para un puntero a objeto de renderizado? Incluso un miembro de puntero que nunca se usa, todavía está ocupando memoria. Por lo tanto, invierta la dirección del puntero: la instancia relacionada con el renderizado hace referencia al modelo de datos (que podría ser, o incluir, su nodo de gráfico de escena), NO viceversa. Y si desea una manera fácil de ejecutar su lista de controladores y obtener acceso a la vista relacionada, use un diccionario / tabla hash, que se acerca al tiempo de acceso de lectura O (1). De esa manera no hay contaminación, y su lógica de simulación no le importa qué procesadores están en su lugar, lo que hace que sus días y noches de codificación Y si desea una manera fácil de ejecutar su lista de controladores y obtener acceso a la vista relacionada, use un diccionario / tabla hash, que se acerca al tiempo de acceso de lectura O (1). De esa manera no hay contaminación, y su lógica de simulación no le importa qué procesadores están en su lugar, lo que hace que sus días y noches de codificación Y si desea una manera fácil de ejecutar su lista de controladores y obtener acceso a la vista relacionada, use un diccionario / tabla hash, que se acerca al tiempo de acceso de lectura O (1). De esa manera no hay contaminación, y su lógica de simulación no le importa qué procesadores están en su lugar, lo que hace que sus días y noches de codificaciónmundos más fáciles.

En cuanto al sacrificio, refiérase a lo anterior. El sacrificio de área de interés es un concepto de lógica de simulación. Es decir, no se procesa el mundo fuera de esta área (generalmente en caja, circular o esférica). Esto tiene lugar en el controlador principal / bucle del juego, antes de que se produzca el renderizado. Por otro lado, el sacrificio de frustum está puramente relacionado con el render. Así que olvídate de sacrificar ahora mismo. No tiene nada que ver con los gráficos de escena, y al enfocarse en él, ocultará el verdadero propósito de lo que está tratando de lograr.

Una nota final ...

Tengo la fuerte sensación de que proviene de un fondo Flash (específicamente AS3), dados todos los detalles sobre la representación incluidos aquí. Sí, el paradigma Flash Stage / DisplayObject incluye toda la lógica de renderizado como parte de la escena. Pero Flash hace muchas suposiciones que no necesariamente quieres hacer. Para un motor de juego completo, es mejor no mezclar los dos, por razones de rendimiento, conveniencia y control de la complejidad del código a través del SoC adecuado .

Ingeniero
fuente
1
Gracias Nick En realidad soy un animador 3D (3D real sin flash) convertido en programador, por lo que tiendo a pensar en términos de gráficos. Si eso no es lo suficientemente malo, comencé en Java y me he estado sacando de la mentalidad de "todo debe ser un objeto" inculcado en ese lenguaje. Me has convencido de que el gráfico de la escena debe estar separado del código de renderizado y de eliminación, ahora mis engranajes están girando exactamente sobre cómo se debe lograr. Estoy pensando en tratar al renderizador como si fuera un sistema propio que hace referencia al gráfico de escena para transformar datos, etc.
Cody Smith
1
@CodySmith, me alegra que haya ayudado. Plug descarado, pero mantengo un marco que se trata de SoC / MVC. Al hacerlo, me encontré con el campamento más tradicional de la industria que insiste en que todo debe estar en un objeto central y monolítico. Pero incluso ellos lo dirían en general: mantenga su representación separada de su gráfico de escena. SoC / SRP es algo que no puedo enfatizar lo suficiente: nunca mezcle más lógica en una sola clase de la que necesita. Incluso recomendaría cadenas de herencia OO complejas sobre lógica mixta en la misma clase, ¡si me pones un arma en la cabeza!
Ingeniero
No, me gusta el concepto. Y tienes razón, esta es la primera mención de SoC que he visto en años de lectura sobre diseño de juegos. Gracias de nuevo.
Cody Smith
@CodySmith Pensó rápidamente mientras navegaba por esto nuevamente. En general, es bueno mantener las cosas desacopladas. Sin embargo, para varios tipos de objetos de controlador de modelo en su base de código que se someten a renderizado, está bien que mantenga colecciones de Renderables (que es una interfaz o clase abstracta) internamente en esos objetos de controlador de modelo centrales. Buenos ejemplos de esto son entidades o elementos de la interfaz de usuario. Por lo tanto, puede acceder rápidamente solo a los renderizadores pertinentes a ese objeto central en particular, sin detalles de implementación que contaminen la clase de entidad, de ahí el uso de interfaces.
Ingeniero
@CodySmith El beneficio es claro con las entidades, que podrían, por ejemplo. tener representaciones tanto en la vista mundial como en un minimapa. De ahí la colección. Alternativamente, puede permitir una sola ranura de representación para cada objeto modelo-controlador, internamente a ese objeto. ¡Pero mantén la interfaz general! Sin detalles, solo Renderer.
Ingeniero