Estou escrevendo um jogo em C ++ usando o OpenGL.
Para quem não sabe, com a API do OpenGL, você faz muitas chamadas para coisas como glGenBuffers
e glCreateShader
etc. Esses tipos de retorno GLuint
são identificadores exclusivos do que você acabou de criar. A coisa que está sendo criada fica na memória da GPU.
Considerando que a memória da GPU às vezes é limitada, você não deseja criar duas coisas iguais quando forem usadas por vários objetos.
Por exemplo, Shaders. Você vincula um programa Shader e, em seguida, possui um GLuint
. Quando terminar o Shader, você deve ligar glDeleteShader
(ou algo a esse respeito).
Agora, digamos que eu tenha uma hierarquia de classes superficial como:
class WorldEntity
{
public:
/* ... */
protected:
ShaderProgram* shader;
/* ... */
};
class CarEntity : public WorldEntity
{
/* ... */
};
class PersonEntity: public WorldEntity
{
/* ... */
};
Qualquer código que eu já vi exigiria que todos os Construtores ShaderProgram*
passassem para ele para serem armazenados no WorldEntity
. ShaderProgram
é minha classe que encapsula a ligação de a GLuint
ao estado atual do shader no contexto OpenGL, além de algumas outras coisas úteis que você precisa fazer com o Shaders.
O problema que tenho com isso é:
- Existem muitos parâmetros necessários para construir um
WorldEntity
(considere que pode haver uma malha, um sombreador, um monte de texturas etc., todos os quais podem ser compartilhados, para que sejam passados como ponteiros) - O que quer que esteja criando as
WorldEntity
necessidades para saber oShaderProgram
que precisa - Provavelmente, isso requer algum tipo de classe gulp
EntityManager
que sabe qual instância do queShaderProgram
passar para diferentes entidades.
Portanto, agora, porque existe uma Manager
classe, as classes precisam se registrar EntityManager
junto com a ShaderProgram
instância de que precisam, ou eu preciso de um imbecil switch
no gerenciador que preciso atualizar para cada novo WorldEntity
tipo derivado.
Meu primeiro pensamento foi criar uma ShaderManager
classe (eu sei, os gerentes são ruins) que passo por referência ou ponteiro para as WorldEntity
classes, para que elas possam criar o ShaderProgram
que quiserem, através do ShaderManager
e ShaderManager
possam acompanhar os ShaderProgram
s já existentes , para que possam retorne um que já exista ou crie um novo, se necessário.
(Eu poderia armazenar os ShaderProgram
s através do hash dos nomes de arquivos do ShaderProgram
código fonte real do s)
Então agora:
- Agora estou passando ponteiros para, em
ShaderManager
vez deShaderProgram
, então ainda há muitos parâmetros - Não preciso de um
EntityManager
, as próprias entidades saberão em que instânciaShaderProgram
criar eShaderManager
manipularão osShaderProgram
s reais . - Mas agora não sei quando é
ShaderManager
possível excluir com segurança umShaderProgram
que ele contém.
Então agora eu adicionei uma contagem de referência à minha ShaderProgram
classe que exclui sua GLuint
via interna glDeleteProgram
e acabo com ela ShaderManager
.
Então agora:
- Um objeto pode criar o
ShaderProgram
que precisar - Mas agora existem
ShaderProgram
s duplicados porque não há um gerente externo acompanhando
Finalmente, tomo uma de duas decisões:
1. Classe estática
Um static class
que é chamado para criar ShaderProgram
s. Ele mantém uma faixa interna de ShaderProgram
s com base em um hash dos nomes de arquivos - isso significa que não preciso mais passar ponteiros ou referências a ShaderProgram
s ou ShaderManager
s ao redor, portanto, menos parâmetros - eles WorldEntities
têm todo o conhecimento sobre a instância ShaderProgram
que desejam criar
Este novo static ShaderManager
precisa:
- manter uma contagem do número de vezes que a
ShaderProgram
é usada e euShaderProgram
não faço nenhuma cópia OU ShaderProgram
s contam suas referências e só chamamglDeleteProgram
seu destruidor quando a contagem é0
ANDShaderManager
periodicamente verifica se háShaderProgram
uma contagem de 1 e as descarta.
As desvantagens dessa abordagem que vejo são:
Eu tenho classe estática global que pode ser um problema. O contexto do OpenGL precisa ser criado antes da chamada de qualquer
glX
função. Então, potencialmente, umWorldEntity
pode ser criado e tente criar umShaderProgram
antes da criação do OpenGL Context, o que resultará em uma falha.A única maneira de contornar isso é repassar tudo ao redor como ponteiros / referências, ou ter uma classe GLContext global que pode ser consultada ou manter tudo em uma classe que cria o Contexto na construção. Ou talvez apenas um booleano global
IsContextCreated
que possa ser verificado. Mas eu me preocupo que isso me dê um código feio em todo lugar.O que eu posso ver é:
- A grande
Engine
classe que tem todas as outras classes ocultas dentro dela para poder controlar a ordem de construção / desconstrução de maneira apropriada. Parece uma grande confusão de código de interface entre o usuário e o mecanismo, como um invólucro sobre um invólucro - Uma série de classes "Manager" que controlam instâncias e excluem coisas quando necessário. Isso pode ser um mal necessário?
- A grande
E
- Quando realmente limpar
ShaderProgram
s fora dostatic ShaderManager
? A cada poucos minutos? Cada loop de jogo? Estou lidando graciosamente com a recompilação de um sombreador no caso em que umShaderProgram
foi excluído, mas depois um novoWorldEntity
solicita; mas tenho certeza de que há uma maneira melhor.
2. Um método melhor
É isso que estou pedindo aqui
fonte
WorldEntity
s; Isso não está mudando parte do problema? Porque agora a classe WorldFactory precisa passar para cada WolrdEntity o ShaderProgram correto.Respostas:
Pedimos desculpas pela necromancia, mas já vi muitos tropeçando em problemas semelhantes com o gerenciamento de recursos OpenGL, incluindo eu no passado. E muitas das dificuldades que lutei com as quais reconheço em outros vieram da tentação de encerrar e, às vezes, abstrair e até encapsular os recursos de OGL necessários para que uma entidade de jogo analógica fosse renderizada.
E o "caminho melhor" que encontrei (pelo menos um que encerrou minhas lutas particulares lá) foi fazer as coisas do contrário. Ou seja, não se preocupe com os aspectos de baixo nível do OGL ao projetar suas entidades e componentes do jogo e afaste-se de idéias como essas que você
Model
deve armazenar como primitivas de triângulo e vértice na forma de quebra de objetos ou até mesmo abstraindo VBOs.Preocupações de renderização vs. preocupações de design de jogos
Existem conceitos de nível um pouco mais alto que as texturas de GPU, por exemplo, com requisitos de gerenciamento mais simples, como imagens de CPU (e você precisa deles de qualquer maneira, pelo menos temporariamente, antes de poder criar e vincular uma textura de GPU). A falta de renderização diz respeito a um modelo pode ser suficiente apenas armazenando uma propriedade indicando o nome do arquivo a ser usado para o arquivo que contém os dados para o modelo. Você pode ter um componente "material" que seja de nível superior e mais abstrato e descreva as propriedades desse material que um shader GLSL.
E existe apenas um lugar na base de código relacionado a coisas como shaders e texturas de GPU e contextos VAOs / VBOs e OpenGL, e é a implementação do sistema de renderização . O sistema de renderização pode percorrer as entidades na cena do jogo (no meu caso, passa por um índice espacial, mas você pode entender mais facilmente e começar com um loop simples antes de implementar otimizações como seleção de frustum com um índice espacial) e descobre seus componentes de alto nível, como "materiais" e "imagens" e nomes de arquivos de modelo.
E seu trabalho é pegar os dados de nível superior que não estão diretamente relacionados à GPU e carregar / criar / associar / vincular / usar / desassociar / destruir os recursos OpenGL necessários com base no que ele descobre em cena e no que está acontecendo com o cena. E isso elimina a tentação de usar coisas como singletons e versões estáticas de "gerentes" e o que não, porque agora todo o seu gerenciamento de recursos OGL está centralizado em um sistema / objeto em sua base de código (embora, é claro, você possa decompô-lo em outros objetos encapsulados pelo renderizador para tornar o código mais gerenciável). Também evita naturalmente alguns pontos de tropeço com coisas como tentar destruir recursos fora de um contexto válido do OGL,
Evitando alterações de design
Além disso, oferece muito espaço para respirar para evitar alterações dispendiosas no design central, porque, em retrospectiva, você descobre que alguns materiais exigem várias passagens de renderização (e vários shaders) para renderizar, como uma passagem de dispersão na subsuperfície e shader para materiais de pele, enquanto anteriormente você queria misturar um material com um único shader da GPU. Nesse caso, não há alteração de design dispendiosa nas interfaces centrais usadas por muitas coisas. Tudo o que você faz é atualizar a implementação local do sistema de renderização para lidar com esse caso anteriormente imprevisto quando encontrar propriedades de capa no seu componente de material de nível superior.
A estratégia geral
E essa é a estratégia geral que uso agora, e ela se torna cada vez mais útil, quanto mais complexas são as suas preocupações com renderização. Como desvantagem, requer um trabalho um pouco mais adiantado do que injetar shaders e VBOs nas entidades do jogo e coisas assim, além de associar mais o seu renderizador ao seu mecanismo de jogo específico (ou suas abstrações, embora em troca o nível superior) entidades e conceitos de jogos tornam-se completamente dissociados das preocupações de renderização de baixo nível). E seu renderizador pode precisar de itens como retornos de chamada para notificá-lo quando as entidades forem destruídas, para que possa desassociar e destruir todos os dados associados a ele (você pode usar a contagem regressiva aqui ou
shared_ptr
para recursos compartilhados, mas apenas localmente dentro do renderizador). E você pode querer uma maneira eficiente de associar e desassociar todos os tipos de renderização de dados a quaisquer entidades em tempo constante (um ECS tende a fornecer isso imediatamente para todos os sistemas, com a forma de associar novos tipos de componentes em tempo real, se você tiver um ECS - caso contrário, não deve ser muito difícil de qualquer maneira) ... mas, de cabeça para baixo, todos esses tipos de coisas provavelmente serão úteis para outros sistemas que não o renderizador.É certo que a implementação real fica muito mais sutil do que isso e pode embaçar um pouco mais essas coisas, como seu mecanismo pode querer lidar com coisas como triângulos e vértices em outras áreas além da renderização (por exemplo: física pode querer que esses dados detectem colisões ) Mas onde a vida começou a ficar muito mais fácil (pelo menos para mim) foi abraçar esse tipo de inversão de mentalidade e estratégia como ponto de partida.
E projetar um renderizador em tempo real é muito difícil na minha experiência - a coisa mais difícil que eu já projetei (e continuo redesenhando) com as rápidas mudanças de hardware, recursos de sombreamento e técnicas descobertas. Mas essa abordagem elimina a preocupação imediata de quando os recursos da GPU podem ser criados / destruídos, centralizando tudo isso na implementação de renderização, e ainda mais benéfico para mim é que ela mudou o que de outra forma seria caro e as alterações em cascata do projeto (que poderiam se espalhar código que não se preocupe imediatamente com a renderização) apenas na implementação do próprio renderizador. E essa redução no custo da mudança pode resultar em enormes economias com algo que muda nos requisitos a cada ano ou dois tão rapidamente quanto a renderização em tempo real.
Seu exemplo de sombreamento
A maneira como abordo seu exemplo de sombreamento é que não me preocupo com coisas como sombreadores GLSL em coisas como entidades de carro e pessoa. Eu me preocupo com "materiais" que são objetos de CPU muito leves que contêm apenas propriedades que descrevem que tipo de material é (pele, pintura de carro, etc.). No meu caso atual, é um pouco sofisticado, pois tenho um DSEL semelhante ao Unreal Blueprints para programar shaders usando um tipo visual de linguagem, mas os materiais não armazenam identificadores de shader GLSL.
Eu costumava fazer coisas semelhantes quando estava armazenando e gerenciando esses recursos "lá fora no espaço" fora do renderizador, porque minhas primeiras tentativas ingênuas que tentavam destruir diretamente esses recursos em um destruidor geralmente tentavam destruí-los fora de um contexto GL válido (e às vezes eu tentava acidentalmente criá-los em script ou algo assim quando não estava em um contexto válido), então eu precisava adiar a criação e a destruição para casos em que eu pudesse garantir que estava em um contexto válido que levam a projetos de "gerente" semelhantes que você descreve.
Todos esses problemas desaparecem se você estiver armazenando um recurso da CPU em seu lugar e se o renderizador lidar com as preocupações do gerenciamento de recursos da GPU. Não posso destruir um shader OGL em nenhum lugar, mas posso destruir um material da CPU em qualquer lugar e usá
shared_ptr
-lo facilmente sem problemas.Agora, essa preocupação é realmente complicada, mesmo no meu caso, se você deseja gerenciar com eficiência os recursos da GPU e descarregá-los quando não for mais necessário. No meu caso, posso lidar com cenas massivas e trabalho em efeitos visuais, em vez de jogos em que os artistas podem ter um conteúdo particularmente intenso não otimizado para renderização em tempo real (texturas épicas, modelos que abrangem milhões de polígonos etc.).
É muito útil para o desempenho não apenas evitar renderizá-los quando estiver fora da tela (fora do cenário de visualização), mas também descarregar os recursos da GPU quando não for mais necessário por um tempo (digamos que o usuário não veja algo muito distante) espaço por um tempo).
Portanto, a solução que costumo usar com mais freqüência é o tipo de solução "com registro de data e hora", embora não tenha certeza de como isso é aplicável aos jogos. Quando começo a usar / vincular recursos para renderização (por exemplo: eles passam no teste de seleção de frustum), armazeno o horário atual com eles. Periodicamente, há uma verificação para ver se esses recursos não são usados há algum tempo e, se forem, são descarregados / destruídos (embora os dados originais da CPU usados para gerar o recurso GPU sejam mantidos até que a entidade real que armazena esses componentes seja destruída ou até que esses componentes sejam removidos da entidade). À medida que o número de recursos aumenta e mais memória é usada, o sistema se torna mais agressivo ao descarregar / destruir esses recursos (a quantidade de tempo ocioso permitido por um antigo,
Eu imagino que depende muito do seu design de jogo. Como se você tiver um jogo com uma abordagem mais segmentada com níveis / zonas menores, poderá (e encontrar o tempo mais fácil para manter as taxas de quadros estáveis) carregar todos os recursos necessários para esse nível com antecedência e descarregá-los quando o o usuário passa para o próximo nível. Considerando que, se você tem um jogo de mundo aberto maciço que é fácil dessa maneira, pode ser necessário uma estratégia muito mais sofisticada para controlar quando criar e destruir esses recursos, e pode haver um desafio maior para fazer tudo isso sem gaguejar. No meu domínio VFX, um pequeno problema para as taxas de quadros não é tão importante (eu tento eliminá-las dentro do razoável), já que o usuário não vai acabar com o jogo por causa disso.
Toda essa complexidade no meu caso ainda está isolada no sistema de renderização e, embora eu tenha generalizado classes e código para ajudar a implementá-lo, não há preocupações com contextos de GL válidos e tentações de usar globais ou qualquer coisa assim.
fonte
Em vez de fazer a contagem de referência na
ShaderProgram
própria classe, é melhor delegar isso para uma classe de ponteiro inteligente, comostd::shared_ptr<>
. Dessa forma, você garante que cada classe tenha apenas um único trabalho a fazer.Para evitar o esgotamento acidental de seus recursos OpenGL, você pode tornar
ShaderProgram
não copiável (construtor de cópias privado / excluído e operador de atribuição de cópia).Para manter um repositório central de
ShaderProgram
instâncias que podem ser compartilhadas, você pode usar umSharedShaderProgramFactory
(semelhante ao seu gerente estático, mas com um nome melhor) como este:A classe de fábrica pode ser implementada como uma classe estática, um Singleton ou uma dependência que é aprovada quando necessário.
fonte
O Opengl foi projetado como uma biblioteca C e possui as características do software procedural. Uma das regras do opengl que vem de ser uma biblioteca C é assim:
Esta é uma característica da API opengl. Basicamente, assume que todo o seu código está dentro da função main () e todos esses identificadores são passados pelas variáveis locais do main ().
As consistências desta regra são as seguintes:
fonte
main
de mim parecer um pouco complicada para mim (pelo menos na redação).