Rolando meu próprio gráfico de cena

23

Olá Game Development SE!

Estou rastreando o OpenGL com a esperança de criar um mecanismo de jogo simples e muito leve. Eu vejo o projeto como uma experiência de aprendizado que pode render um pouco de dinheiro no final, mas será divertido de qualquer maneira.

Até agora, usei o GLFW para obter E / S básica, uma janela (com uma tecla de tela cheia tão sofisticada do F11) e, é claro, um contexto OpenGL. Eu também usei o GLEW para expor o restante das extensões do OpenGL porque estou usando o Windows e quero usar todo o OpenGL 3.0+.

O que me leva ao gráfico da cena. Em suma, eu gostaria de fazer o meu próprio. Essa decisão veio depois de analisar o OSG e ler alguns artigos sobre como o conceito de um gráfico de cena se tornou distorcido, torto e quebrado. Um desses artigos descreveu como os gráficos de cena se desenvolveram como ...

Em seguida, adicionamos todo esse material extra, como pendurar enfeites em uma árvore de Natal, exceto que alguns enfeites são bons bifes suculentos e outros são vacas vivas inteiras.

Seguindo a analogia, eu gostaria do bife, a carne do que um gráfico de cena deveria ser, sem ter que amarrar pilhas de código extra ou vacas inteiras.

Então, com isso em mente, me pergunto exatamente o que um gráfico de cena deve ser e como um gráfico de cena simples deve ser implementado? Aqui está o que eu tenho até agora ...

Um único pai, árvore n-children ou DAG que ...

  • Deve acompanhar as transformações dos objetos do jogo (posição, rotação, escala)
  • Deve manter estados de renderização para otimizações
  • Deve fornecer um meio de selecionar objetos fora da vista frustum

Com as seguintes propriedades ...

  • Todos os nós devem ser tratados como renderizáveis ​​(mesmo que não sejam renderizados). Isso significa que eles ...

    • Todos devem ter métodos cull (), state () e draw () (retornar 0 se não estiver visível)
    • cull () chama recursivamente cull () em todos os filhos, gerando uma malha de descarte completa para todo o nó e todos os filhos. Outro método, hasChanged () pode permitir que as chamadas malhas estáticas não precisem ter sua geometria de descarte computada em cada quadro. Isso funcionaria de tal forma que, se algum nó da subárvore foi alterado, toda a geometria até a raiz é reconstruída.
  • Os estados de renderização serão mantidos em uma enumeração simples, cada nó selecionará nessa enumeração um conjunto de estados OpenGL que ele exige e esse estado será configurado antes que draw () seja chamado nesse nó. Isso permite o lote, todos os nós de um determinado conjunto de estados serão renderizados juntos, o próximo conjunto de estados será configurado e assim por diante.

  • Nenhum nó deve conter diretamente dados de geometria / sombreador / textura, em vez disso, os nós devem apontar para objetos compartilhados (talvez gerenciados por algum objeto singleton, como um gerenciador de recursos).

  • Os gráficos de cena devem ser capazes de referenciar outros gráficos de cena (talvez usando um nó proxy) para permitir situações como esta , permitindo que modelos / objetos complexos de várias malhas sejam copiados ao redor do gráfico de cena sem adicionar uma tonelada de dados.

Espero obter um feedback valioso sobre o meu design atual. Está faltando funcionalidade? Existe um padrão de design / maneira muito melhor? Estou perdendo algum conceito maior que será necessário incluir neste design para um jogo 3D um tanto simples? Etc.

Obrigado, -Cody

Cody Smith
fonte

Respostas:

15

O conceito

Fundamentalmente, um gráfico de cena nada mais é do que um gráfico acíclico bidirecional que serve para representar um conjunto hierárquico de relações espaciais.

Os motores em estado selvagem tendem a incluir outros itens no gráfico da cena, conforme observado. Se você vê isso como a carne ou a vaca provavelmente depende da sua experiência com motores e bibliotecas por aí.

Mantendo o peso leve

Eu sou a favor do estilo Unity3D de ter o nó do gráfico de cena (que no fundo é uma estrutura topológica e não espacial / topográfica) inclui inerentemente parâmetros e funcionalidades espaciais. No meu mecanismo, meus nós são ainda mais leves que o Unity3D, onde herdam muitos membros indesejados de superclasses / interfaces implementadas: Aqui está o que eu tenho - o mais leve possível:

  • membros de ponteiro pai / filho.
  • membros do parâmetro espacial de pré-transformação: posição xyz, pitch, yaw e roll.
  • uma matriz de transformação; as matrizes em uma cadeia hierárquica podem multiplicar-se de maneira muito rápida e fácil, caminhando recursivamente para cima / para baixo na árvore, fornecendo as transformações espaciais hierárquicas que são a principal característica de um gráfico de cena;
  • um updateLocal()método que atualiza apenas as matrizes de transformação desse
  • um updateAll()método que atualiza esta e todas as matrizes de transformação de nós descendentes

... Também incluo lógica de equações de movimento e, portanto, membros de velocidade / aceleração (lineares e angulares) na minha classe de nós. Você pode renunciar a isso e manipulá-lo em seu controlador principal, se desejar. Mas é isso - muito leve mesmo. Lembre-se, você pode tê-los em milhares de entidades. Então, como você sugeriu, mantenha a luz.

Construindo hierarquias

O que você diz sobre um gráfico de cena que faz referência a outros gráficos de cena ... Estou esperando a linha de argumento? Claro que sim. Esse é o uso principal deles. Você pode adicionar qualquer nó a qualquer outro nó, e as transformações devem ocorrer automaticamente no espaço local da nova transformação. Tudo o que você está fazendo é mudar um ponteiro, não é como se você estivesse copiando dados! Ao alterar um ponteiro, você tem um gráfico de cena mais profundo. Se o uso de Proxies torna as coisas mais eficientes, é claro, mas nunca vi a necessidade.

Evitar lógica relacionada à renderização

Esqueça a renderização enquanto escreve sua classe de nó do gráfico de cena, ou você confunde tudo. O que importa é que você tenha um modelo de dados - seja o gráfico da cena ou não, não importa - e que algum representante irá inspecionar esse modelo de dados e renderizar objetos no mundo de acordo, seja em 1, 2 , 3 ou 7 dimensões. O que estou dizendo é: Não contamine seu gráfico de cena com a lógica de renderização. Um gráfico de cena é sobre topologia e topografia - isto é, conectividade e características espaciais. Esse é o verdadeiro estado da simulação e existe mesmo na ausência de renderização (que pode assumir qualquer forma sob o sol, desde a visualização em primeira pessoa até o gráfico estatístico e a descrição textual). Os nós não apontam para objetos relacionados à renderização - no entanto, o contrário pode ser verdadeiro. Considere também isso: Nem todo nó do gráfico de cena em sua árvore inteira será renderizável. Muitos serão apenas contêineres. Então, por que alocar memória para um ponteiro para renderizar objeto? Mesmo um membro ponteiro que nunca é usado, ainda está ocupando memória. Portanto, inverta a direção do ponteiro: a instância relacionada à renderização faz referência ao modelo de dados (que pode ser, ou incluir, o nó do gráfico de cena), NÃO vice-versa. E se você deseja uma maneira fácil de percorrer sua lista de controladores e ainda obter acesso à visualização relacionada, use um dicionário / hashtable, que se aproxime de O (1) tempo de acesso de leitura. Dessa forma, não há contaminação, e sua lógica de simulação não se importa com os renderizadores, o que torna seus dias e noites de codificação Então, por que alocar memória para um ponteiro para renderizar objeto? Mesmo um membro ponteiro que nunca é usado, ainda está ocupando memória. Portanto, inverta a direção do ponteiro: a instância relacionada à renderização faz referência ao modelo de dados (que pode ser, ou incluir, o nó do gráfico de cena), NÃO vice-versa. E se você deseja uma maneira fácil de percorrer sua lista de controladores e ainda obter acesso à visualização relacionada, use um dicionário / hashtable, que se aproxime de O (1) tempo de acesso de leitura. Dessa forma, não há contaminação, e sua lógica de simulação não se importa com os renderizadores, o que torna seus dias e noites de codificação Então, por que alocar memória para um ponteiro para renderizar objeto? Mesmo um membro ponteiro que nunca é usado, ainda está ocupando memória. Portanto, inverta a direção do ponteiro: a instância relacionada à renderização faz referência ao modelo de dados (que pode ser, ou incluir, o nó do gráfico de cena), NÃO vice-versa. E se você deseja uma maneira fácil de percorrer sua lista de controladores e ainda obter acesso à visualização relacionada, use um dicionário / hashtable, que se aproxime de O (1) tempo de acesso de leitura. Dessa forma, não há contaminação, e sua lógica de simulação não se importa com os renderizadores, o que torna seus dias e noites de codificação E se você deseja uma maneira fácil de percorrer sua lista de controladores e ainda obter acesso à visualização relacionada, use um dicionário / hashtable, que se aproxime de O (1) tempo de acesso de leitura. Dessa forma, não há contaminação, e sua lógica de simulação não se importa com os renderizadores, o que torna seus dias e noites de codificação E se você deseja uma maneira fácil de percorrer sua lista de controladores e ainda obter acesso à visualização relacionada, use um dicionário / hashtable, que se aproxime de O (1) tempo de acesso de leitura. Dessa forma, não há contaminação, e sua lógica de simulação não se importa com os renderizadores, o que torna seus dias e noites de codificaçãomundos mais fáceis.

Quanto ao descarte, consulte o item acima. A seleção por área de interesse é um conceito de lógica de simulação. Ou seja, você não processa o mundo fora desta área (geralmente em caixas, circular ou esférica). Isso ocorre no loop principal do controlador / jogo, antes da renderização. Por outro lado, o descarte de frustum é puramente relacionado à renderização. Então esqueça a seleção agora. Não tem nada a ver com gráficos de cenas e, ao focar neles, você estará obscurecendo o verdadeiro propósito do que está tentando alcançar.

Uma nota final ...

Tenho a forte sensação de que você é proveniente de um background em Flash (especificamente AS3), considerando todos os detalhes sobre a renderização incluídos aqui. Sim, o paradigma Flash Stage / DisplayObject inclui toda a lógica de renderização como parte do gráfico de cenário. Mas o Flash faz muitas suposições que você não necessariamente quer fazer. Para um mecanismo de jogo completo, é melhor não misturar os dois, por razões de desempenho, conveniência e controle da complexidade do código por meio do SoC adequado .

Engenheiro
fonte
1
Obrigado Nick. Na verdade, sou um animador 3D (3D real, não flash), que se tornou programador, portanto, tendo a pensar em termos de gráficos. Se isso não é ruim o suficiente, eu comecei em Java e fui me esquivando da mentalidade "tudo deve ser um objeto" incutida nessa linguagem. Você me convenceu de que o gráfico de cena deve ser separado do código de renderização e seleção, agora minhas engrenagens estão girando sobre exatamente como isso deve ser realizado. Estou pensando em tratar o renderizador como se fosse um sistema distinto que referencie o gráfico de cena para transformar dados etc.
Cody Smith
1
@CodySmith, que bom que ajudou. Plugue sem vergonha, mas mantenho uma estrutura que trata de SoC / MVC. Ao fazê-lo, cheguei ao ponto mais tradicional da indústria, que insiste que tudo deveria estar em um objeto monolítico central. Mas mesmo eles diriam em geral para você - mantenha sua renderização separada do gráfico da cena. SoC / SRP é algo que não posso enfatizar o suficiente - nunca misture mais lógica em uma única classe do que você precisa. Eu até defenderia cadeias complexas de herança OO em vez de lógica mista na mesma classe, se você colocasse uma arma na minha cabeça!
Engineer
Não, eu gosto do conceito. E você está certo, esta é a primeira menção ao SoC que eu já vi em algum lugar nos últimos anos lendo sobre Design de jogos. Obrigado novamente.
Cody Smith
@CodySmith Pensou rapidamente enquanto navegava sobre isso novamente. Em geral, é bom manter as coisas dissociadas. Para vários tipos de objetos de controlador de modelo em sua base de código que são renderizados, no entanto, é bom manter coleções de Renderables (que é uma classe de interface ou abstrata) internamente para esses objetos principais de controlador de modelo. Bons exemplos disso são entidades ou elementos da interface do usuário. Assim, você pode acessar rapidamente apenas os renderizadores pertinentes a esse objeto principal em particular - sem especificações de implementação que contaminariam a classe de entidade, daí o uso de interfaces.
Engenheiro de
@CodySmith O benefício é claro para as entidades, que podem, por exemplo. tem representações na viewport do mundo e em um minimapa. Portanto, a coleção. Como alternativa, você pode permitir apenas um único slot de renderizador para cada objeto de controlador de modelo internamente para esse objeto. Mas mantenha a interface geral! Sem detalhes - apenas Renderer.
Engenheiro de