Fundo:
Estou projetando um sistema de renderização 3D simples para uma arquitetura de tipo de sistema de componente de entidade usando C ++ e OpenGL. O sistema consiste em um renderizador e um gráfico de cena. Quando eu terminar a primeira iteração do renderizador, posso distribuir o gráfico da cena na arquitetura do ECS. Por enquanto, é um espaço reservado de uma maneira ou de outra. Se possível, meus objetivos para o renderizador a seguir:
- Simplicidade . Isto é para um projeto de pesquisa e eu quero poder mudar e expandir meus sistemas com facilidade (daí a abordagem ECS).
- Performance . Minha cena pode ter muitos modelos pequenos e também grandes volumes com muita geometria. Não é aceitável adquirir objetos do contexto OGL e geometria do buffer a cada quadro de renderização. Estou buscando a localidade dos dados para evitar falhas de cache.
- Flexibilidade . Ele deve ser capaz de renderizar sprites, modelos e volumes (voxels).
- Desacoplado . O gráfico da cena pode ser refatorado na arquitetura central do ECS depois que eu escrever meu renderizador.
- Modular . Seria bom poder trocar diferentes renderizadores sem alterar meu gráfico de cena.
- Transparência referencial , o que significa que, a qualquer momento, posso fornecer uma cena válida e ela sempre renderiza a mesma imagem para essa cena. Este objetivo em particular não é necessariamente necessário. Eu pensei que isso ajudaria a simplificar a serialização de cenas (precisarei ser capaz de salvar e carregar cenas) e me dar flexibilidade para trocar cenas diferentes durante o tempo de execução para fins de teste / experimentação.
Problema e idéias:
Eu vim com algumas abordagens diferentes para tentar, mas estou tendo dificuldades em saber como armazenar em cache os recursos do OGL (VAO, VBOs, shaders etc.) para cada nó de renderização. A seguir, estão os diferentes conceitos de cache que eu pensei até agora:
- Cache centralizado. Cada nó da cena possui um ID e o renderizador possui um cache que mapeia os IDs para renderizar os nós. Cada nó de renderização contém os VAO e VBOs associados à geometria. Uma falta de cache adquire recursos e mapeia a geometria para um nó de renderização no cache. Quando a geometria é alterada, um sinalizador sujo é definido. Se o renderizador vir um sinalizador de geometria suja ao iterar pelos nós da cena, ele rebuffará os dados usando o nó de renderização. Quando um nó de cena é removido, um evento é transmitido e o renderizador remove o nó de renderização associado do cache enquanto libera recursos. Como alternativa, o nó é marcado para remoção e o representante é responsável por removê-lo. Eu acho que essa abordagem atinge mais de perto o objetivo 6, considerando também 4 e 5. 2 sofre com a complexidade extra e a perda de localidade dos dados com pesquisas de mapa em vez de acesso à matriz.
- Cache distribuído . Semelhante acima, exceto que cada nó da cena possui um nó de renderização. Isso ignora a pesquisa do mapa. Para endereçar a localidade dos dados, os nós de renderização podem ser armazenados no renderizador. Em seguida, os nós da cena poderiam ter ponteiros para renderizar os nós e o renderizador define o ponteiro em uma falta de cache. Eu acho que esse tipo de imita uma abordagem de componente de entidade, por isso seria consistente com o restante da arquitetura. O problema aqui é que agora os nós da cena retêm dados específicos da implementação do renderizador. Se eu mudar a forma como as coisas são renderizadas no renderizador (como renderizar sprites x volumes), agora preciso alterar o nó de renderização ou adicionar mais "componentes" ao nó da cena (o que significa alterar também o gráfico da cena). No lado positivo, essa parece ser a maneira mais simples de colocar meu renderizador de primeira iteração em funcionamento.
- Metadados distribuídos . Um componente de metadados do cache do renderizador é armazenado em cada nó da cena. Esses dados não são específicos da implementação, mas contêm um ID, tipo e qualquer outro dado relevante necessário ao cache. A pesquisa de cache pode ser feita diretamente em uma matriz usando o ID, e o tipo pode indicar qual tipo de abordagem de renderização usar (como sprites x volumes).
- Visitante + mapeamento distribuído . O renderizador é um visitante e os nós da cena são elementos no padrão do visitante. Cada nó da cena contém uma chave de cache (como os metadados, mas apenas um ID) que apenas o renderizador manipula. O ID pode ser usado para matriz em vez de consulta geral do mapa. O renderizador pode permitir que o nó da cena despache uma função de renderização diferente com base no tipo do nó da cena, e o ID pode ser usado por qualquer cache. Um ID padrão ou fora do intervalo indicaria uma falta de cache.
Como resolveria este problema? Ou você tem alguma sugestão? Obrigado por ler minha parede de texto!
Respostas:
Depois de reler sua pergunta, sinto fortemente que você está complicando demais o problema. Aqui está o porquê:
Na verdade, existem apenas dois tipos de sistema de renderização: Encaminhar e Adiado, nenhum dos quais depende de um gráfico de cena.
Os únicos problemas de desempenho que você realmente deve enfrentar em qualquer sistema de renderização devem vir da alta contagem de polis e do ineficiente sombreador e código do cliente.
As falhas de cache realmente reduzem o desempenho, mas não são exatamente os monstros que você pensa que são. 80% de suas melhorias de desempenho serão de um algoritmo mais eficiente. Não cometa o erro de pré-otimizar seu código.
Dito isto:
Se você estiver usando um gráfico de cenário de homebrew, já deve estar usando uma interface "Renderer" (ou classe base) para projetar a parte de renderização do código do gráfico de cenário. O padrão de visitante usando o envio duplo é uma boa abordagem para isso, pois você pode estar usando muitos tipos de nós de gráfico, como cor, textura, malha, transformação etc. Dessa forma, durante o ciclo de renderização, tudo o que o renderizador precisa fazer é percorra a estrutura da árvore da cena em profundidade, passando por si mesma como argumento. Dessa maneira, o renderizador é basicamente apenas uma coleção de shaders e talvez um buffer de moldura ou dois. O resultado disso é que o código de pesquisa / remoção não é mais necessário para o sistema de renderização, apenas o próprio cenário.
Certamente existem outras maneiras de resolver os problemas que você enfrenta, mas não quero dar uma resposta por muito tempo. Portanto, meu melhor conselho é conseguir que algo simples funcione primeiro, depois expandi-lo para descobrir suas fraquezas, depois experimentar outras abordagens e ver onde estão seus pontos fortes / fracos na prática.
Então você estará bem posicionado para tomar uma decisão informada.
fonte