Estou escrevendo um jogo usando C ++ e OpenGL 2.1. Eu estava pensando em como eu poderia separar os dados / lógica da renderização. No momento, uso uma classe base 'Renderable' que fornece um método virtual puro para implementar o desenho. Mas todo objeto tem um código tão especializado que somente ele sabe como definir adequadamente os uniformes de shader e organizar os dados do buffer da matriz de vértices. Acabo com muitas chamadas de função gl * por todo o meu código. Existe alguma maneira genérica de desenhar os objetos?
21
m_renderable
membro. Dessa forma, você pode separar melhor sua lógica. Não imponha a "interface" renderizável em objetos gerais que também possuem física, ai e outros enfeites. Depois disso, você pode gerenciar os renderizáveis separadamente. Você precisa de uma camada de abstração nas chamadas de função do OpenGL para dissociar ainda mais as coisas. Portanto, não espere que um bom mecanismo tenha chamadas de API GL em suas várias implementações renderizáveis. É isso, em poucas palavras.Respostas:
Uma idéia é usar o padrão de design do Visitor. Você precisa de uma implementação do Renderer que saiba renderizar adereços. Todo objeto pode chamar a instância do renderizador para manipular o trabalho de renderização.
Em algumas linhas de pseudocódigo:
O material gl * é implementado pelos métodos do renderizador, e os objetos armazenam apenas os dados necessários para renderização, posição, tipo de textura, tamanho ... etc.
Além disso, você pode configurar diferentes renderizadores (debugRenderer, hqRenderer, ... etc) e usá-los dinamicamente, sem alterar os objetos.
Isso também pode ser fácil combinado com os sistemas de entidade / componente.
fonte
Entity/Component
um pouco mais a alternativa, pois ela pode ajudar a separar os provedores de geometria de outras partes do motor (IA, Física, Redes ou jogabilidade geral). +1!ObjectA
eObjectB
porDrawableComponentA
eDrawableComponentB
, dentro e tornar métodos, utilizar outros componentes, se você precisar dele, como:position = component->getComponent("Position");
E no circuito principal, você tem uma lista de componentes drawable para chamar desenhar com.Renderable
) que possui umadraw(Renderer&)
função e todos os objetos que podem ser renderizados os implementam? Nesse caso,Renderer
só precisa de uma função que aceite qualquer objeto que implemente a interface e a chamada comunsrenderable.draw(*this);
?gl_*
funções para o renderizador (separando a lógica da renderização), mas sua solução move asgl_*
chamadas para os objetos.Sei que você já aceitou a resposta de Zhen, mas gostaria de colocar outra por aí, caso isso ajude mais alguém.
Para reiterar o problema, o OP deseja manter o código de renderização separado da lógica e dos dados.
Minha solução é usar uma classe diferente todos juntos para renderizar o componente, que é separado da
Renderer
classe e da lógica. Primeiro, é necessário haver umaRenderable
interface que tenha uma funçãobool render(Renderer& renderer);
e aRenderer
classe use o padrão de visitante para recuperar todas asRenderable
instâncias, dada a lista de seGameObject
renderiza os objetos que possuem umaRenderable
instância. Dessa forma, o Renderer não precisa saber de cada tipo de objeto existente e ainda é responsabilidade de cada tipo de objeto informá-lo porRenderable
meio dagetRenderable()
função. Ou, como alternativa, você pode criar umaRenderableVisitor
classe que visite todos os GameObjects e, com base nasGameObject
condições individuais , eles podem optar por adicionar / não adicionar sua renderização ao visitante. De qualquer forma, o principal é que ogl_*
todas as chamadas estão fora do próprio objeto e residem em uma classe que conhece detalhes íntimos do próprio objeto, em vez de fazer parte deleRenderer
.AVISO LEGAL : Eu escrevi essas classes manualmente no editor, então há uma boa chance de que eu tenha perdido algo no código, mas espero que você entenda.
Para mostrar um exemplo (parcial):
Renderable
interfaceGameObject
classe:Renderer
Classe (parcial) .RenderableObject
classe:ObjectA
classe:ObjectARenderable
classe:fonte
Construa um sistema de comando de renderização. Um objeto de alto nível, que tem acesso aos
OpenGLRenderer
objetos de cenário e de jogo / game, iterará o gráfico da cena ou os objetos de jogo e criará um loteRenderCmds
, que será submetido aoOpenGLRenderer
que irá desenhar cada um por sua vez e, portanto, contendo todo o OpenGL código relacionado.Há mais vantagens nisso do que apenas abstração; eventualmente, à medida que sua complexidade de renderização aumenta, você pode classificar e agrupar cada comando de renderização por textura ou sombreador, por exemplo,
Render()
para eliminar muitos gargalos nas chamadas de desenho que podem fazer uma enorme diferença no desempenho.fonte
Depende completamente se você pode fazer suposições sobre o que é comum para todas as entidades renderizáveis ou não. No meu mecanismo, todos os objetos são renderizados da mesma maneira, portanto, basta fornecer vbos, texturas e transformações. Em seguida, o renderizador busca todos eles, portanto, nenhuma chamada de função do OpenGL é necessária nos diferentes objetos.
fonte
Definitivamente coloque o código de renderização e a lógica do jogo em diferentes classes. A composição (como sugerido pelo teodron) é provavelmente a melhor maneira de fazer isso; cada Entidade no mundo do jogo terá seu próprio Renderable - ou talvez um conjunto deles.
Você ainda pode ter várias subclasses de Renderable, por exemplo, para lidar com animação esquelética, emissores de partículas e shaders complexos, além do shader básico de textura e iluminação. A classe Renderable e suas subclasses devem conter apenas as informações necessárias para a renderização: geometria, texturas e shaders.
Além disso, você deve separar uma instância de uma determinada malha da própria malha. Digamos que você tenha centenas de árvores na tela, cada uma usando a mesma malha. Você deseja armazenar a geometria apenas uma vez, mas precisará de matrizes de localização e rotação separadas para cada árvore. Objetos mais complexos, como humanóides animados, também terão informações adicionais sobre o estado (como um esqueleto, o conjunto de animações atualmente aplicadas etc.).
Para renderizar, a abordagem ingênua é iterar sobre todas as entidades do jogo e dizer a ela para se render. Como alternativa, cada entidade (quando gera) pode inserir seus objetos renderizáveis em um objeto de cena. Então, sua função de renderização informa à cena para renderizar. Isso permite que a cena faça coisas complexas relacionadas à renderização sem incorporar esse código nas entidades do jogo ou em uma subclasse renderizável específica.
fonte
Esse conselho não é realmente específico da renderização, mas deve ajudar a criar um sistema que mantenha as coisas em grande parte separadas. Primeiro, tente manter os dados 'GameObject' separados das informações de posição.
Vale a pena notar que informações posicionais simples de XYZ podem não ser tão simples. Se você estiver usando um mecanismo de física, os dados de posição poderão ser armazenados no mecanismo de terceiros. Você precisaria sincronizar entre eles (o que envolveria muita cópia inútil da memória) ou consultar as informações diretamente do mecanismo. Mas nem todos os objetos precisam de física, alguns serão fixados no local, então um simples conjunto de carros alegóricos funciona bem lá. Alguns podem até ser anexados a outros objetos, portanto, sua posição é na verdade um deslocamento de outra posição. Em uma configuração avançada, você pode ter a posição armazenada apenas na GPU; o único momento em que seria necessário no lado do computador é para scripts, armazenamento e replicação de rede. Portanto, você provavelmente terá várias opções possíveis para seus dados posicionais. Aqui faz sentido usar herança.
Em vez de um objeto possuir sua posição, ele deve pertencer a uma estrutura de dados de indexação. Por exemplo, um 'Nível' pode ter um Octree, ou talvez uma 'cena' de um mecanismo de física. Quando você deseja renderizar (ou configurar uma cena de renderização), consulta sua estrutura especial em busca de objetos visíveis para a câmera.
Isso também ajuda a fornecer um bom gerenciamento de memória. Dessa forma, um objeto que não está realmente em uma área nem sequer tem uma posição que faça sentido, em vez de retornar 0,0 ou mais cordões que ele tinha na última vez em uma área.
Se você não mantiver mais as coordenadas no objeto, em vez de object.getX (), você acabará tendo level.getX (objeto). O problema com isso é procurar o objeto no nível provavelmente será uma operação lenta, pois terá que examinar todos os seus objetos e corresponder ao que você está consultando.
Para evitar isso, eu provavelmente criaria uma classe 'link' especial. Um que se liga entre um nível e um objeto. Eu chamo isso de "Localização". Isso conteria as coordenadas xyz, bem como a alça para o nível e uma alça para o objeto. Essa classe de link seria armazenada na estrutura / nível espacial e o objeto teria uma referência fraca a ela (se o nível / localização for destruído, a atualização dos objetos precisará ser atualizada para nula. Também pode valer a pena ter a classe Location realmente 'possua' o objeto, assim, se um nível for excluído, a estrutura especial do índice, os locais que ele contém e seus Objetos.
Agora, as informações de posição são armazenadas apenas em um único local. Não duplicado entre o objeto, a estrutura de indexação espacial, o renderizador e assim por diante.
Estruturas de dados espaciais como Octrees geralmente nem precisam ter as coordenadas dos objetos que armazenam. Essa posição é armazenada na localização relativa dos nós na própria estrutura (pode ser considerada como uma espécie de compactação com perdas, sacrificando a precisão por tempos de pesquisa rápidos). Com o objeto de localização no Octree, as coordenadas reais são encontradas dentro dele depois que a consulta é concluída.
Ou, se você estiver usando um mecanismo de física para gerenciar as localizações dos objetos ou uma mistura entre as duas, a classe Location deve lidar com isso de forma transparente, mantendo todo o seu código em um único local.
Outra vantagem é agora a posição e a referência ao nível são armazenadas no mesmo local. Você pode implementar object.TeleportTo (other_object) e fazê-lo funcionar em vários níveis. Da mesma forma, a descoberta de caminhos da IA pode seguir algo em uma área diferente.
Com relação à renderização. Sua renderização pode ter uma ligação semelhante ao local. Exceto que teria o material específico de renderização lá. Você provavelmente não precisa que o 'Objeto' ou 'Nível' seja armazenado nessa estrutura. O objeto pode ser útil se você estiver tentando fazer algo como escolher cores ou renderizar uma barra de hit flutuando acima dele, mas, caso contrário, o renderizador se preocupa apenas com a malha e tal. RenderableStuff seria uma malha, também poderia ter caixas delimitadoras e assim por diante.
Talvez você não precise fazer isso em todos os quadros, pode garantir uma região maior do que a câmera atualmente mostra. Coloque em cache, rastreie os movimentos do objeto para ver se a caixa delimitadora está ao alcance, rastreie o movimento da câmera e assim por diante. Mas não comece a mexer com esse tipo de coisa até que você o compare.
O próprio mecanismo de física pode ter uma abstração semelhante, pois também não precisa dos dados do objeto, apenas da malha de colisão e das propriedades físicas.
Todos os dados do objeto principal conteriam o nome da malha que o objeto usa. O mecanismo de jogo pode então prosseguir e carregá-lo no formato que desejar, sem sobrecarregar sua classe de objeto com várias coisas específicas de renderização (que podem ser específicas à sua API de renderização, por exemplo, DirectX vs OpenGL).
Também mantém diferentes componentes separados. Isso facilita fazer coisas como substituir o seu mecanismo de física, já que essas coisas são praticamente independentes em um único local. Isso também facilita muito o desestímulo. Você pode testar itens como consultas de física sem ter que configurar objetos falsos reais, pois tudo o que você precisa é da classe Location. Você também pode otimizar as coisas mais facilmente. Isso torna mais óbvias as consultas que você precisa executar em quais classes e locais únicos para otimizá-las (por exemplo, o level.getVisibleObject acima seria o local onde você poderia armazenar em cache as coisas se a câmera não se mover muito).
fonte