«Love and the Deep Sky», um jogo para telemóvel interativo de namoro imersivo em 3D surrealista, ultrapassou os 70 milhões de jogadores em todo o mundo desde o seu lançamento, a 18 de janeiro de 2024, e foi galardoado com o prémio de Melhor Jogo para Telemóvel na Gamescom 2025.
No evento Unite Shanghai 2025 de hoje, os engenheiros de motores gráficos do jogo «Love & Deep Space» realizaram, pela primeira vez, uma apresentação técnica aprofundada, revelando os bastidores da criação do jogo e explicando como, através de um pipeline de renderização de nível cinematográfico e de efeitos físicos realistas e detalhados, criam um mundo realista e tangível para os jogadores!
△A sala estava lotada durante a sessão de partilha
Tornou-se a sessão mais badalada da Unity Conference deste ano
Na Diezhi, a partilha e a criação fazem parte do nosso dia-a-dia. Ao longo do último ano, a Diezhi organizou cerca de 40 sessões de partilha internas, com um total acumulado de 5 000 participações de colaboradores!Destas, as partilhas técnicas representaram mais de 50%! Os oradores provêm de várias equipas de produção e do departamento técnico, e os conteúdos abrangem experiências práticas e explorações de vanguarda em diferentes áreas. Num ambiente de comunicação aberto, o conhecimento especializado e as ideias circulam livremente, estimulando-se plenamente!
Segue-se uma recapitulação do conteúdo completo da sessão de partilha técnica sobre «Love and the Deep Space»:
O conteúdo partilhado sobre a estrutura subjacente de renderização de «Love and Deep Space» abrange principalmente três partes: [otimização da renderização de cenários, esquemas de iluminação e conceção do pipeline, e otimização de sombras].Durante o processo de desenvolvimento, baseámo-nos no Unity 2019 para realizar modificações profundas no código-fonte do motor, tendo desenvolvido um pipeline SRP personalizado. Atualmente, a versão online para Android utiliza GLES 3.1, mas no futuro será lançada também uma versão Vulkan, com o objetivo de melhorar continuamente o desempenho e satisfazer as expectativas dos jogadores em relação a jogos de alta qualidade.
1. Otimização da renderização de cenários
No âmbito da otimização da renderização de cenários, desenvolvemos um sistema de renderização de cenários denominado «RendererGroupRenderer», no qual cada lote de renderização é designado como um «RenderGroup». Através deste sistema, implementámos as seguintes funcionalidades:
- Descrição de cenários estáticos personalizados: removemos os GameObjects, evitando assim a perda de desempenho resultante da atualização de um grande número de GameObjects.
- Otimização da frequência de upload da CPU para a GPU: inclui principalmente o upload de InstanceData e ConstantBuffer. No que diz respeito à otimização do upload de InstanceData, na fase inicial do projeto, para cenários interiores de pequena escala, adotámos a geração estática do InstanceDataBuffer, combinada com o recorte por segmentação BVH.À medida que o projeto avançava, os requisitos de precisão dos cenários foram aumentando constantemente, pelo que, numa fase posterior, passámos a realizar o recorte e o preenchimento de instâncias no lado da GPU. A otimização do upload do ConstantBuffer será explicada em pormenor na secção seguinte, intitulada «Otimização do desempenho por DrawCall».
- Recorte preliminar paralelo no lado da CPU através do sistema Burst+Job: para objetos estáticos, implementámos um sistema de recorte altamente concorrente através do sistema Burst+Job, realizando apenas o recorte preliminar no lado da CPU e delegando as tarefas de recorte de alta precisão à GPU.
Formato dos dados do InstanceData
A utilização de InstanceData na forma de Constant Buffer, comum na indústria, apresenta algumas desvantagens, tais como o limite de tamanho de 64 KB, a pequena capacidade do buffer constante e a facilidade com que se verifica um «cache breakthrough» durante a indexação dinâmica, o que leva a uma diminuição do desempenho.Outra forma comum consiste em utilizar SSBO para transmitir InstanceData, mas o desempenho de leitura deste método é normalmente inferior ao do ConstantBuffer quando não ocorre esgotamento do buffer; além disso, alguns dispositivos Android não suportam a leitura de SSBO no vertex shader no GLES, o que também limita a sua compatibilidade.Além disso, ambas as soluções partilham um problema comum: dependem da indexação dinâmica, o que prejudica o desempenho em telemóveis de gama baixa.
Em resposta às questões acima referidas, propomos uma solução do tipo «vinho velho em garrafa nova» — os Dados de Instância Baseados em Vertex Stream.
- Utilizar o Vertex Stream do PerInstance Step como Instance Buffer;
- Utiliza a cache do Vertex Fetch, não requer índices dinâmicos, apresenta uma elevada taxa de acertos na cache e não apresenta problemas de compatibilidade;
- A saída para o Instance VertexBuffer é efetuada através do ComputeShader, permitindo o funcionamento orientado pela GPU.
Utilizar o Vertex Stream do PerInstance Step como Instance Buffer — trata-se de um método de instância suportado desde o início da GPU Instancing, que permite evitar tanto os problemas de desempenho decorrentes da indexação dinâmica como os problemas de compatibilidade do SSBO.Também podemos implementar um sistema «GPU Driven» com elevada compatibilidade com GLES através da saída do Compute Shader para o Instance Vertex Buffer.
Por fim, uma vez que o motor Unity não suporta, a nível de base, o Vertex Stream com PerInstance Step, procedemos a uma personalização correspondente do motor, tendo acabado por disponibilizar à camada superior uma interface DrawMeshInstancedTraditional adicionada ao CommandBuffer, que requer a passagem de outra malha como dados de instância.Adicionámos também as interfaces necessárias para configurar a semântica dos vértices correspondentes a cada segmento de dados na malha de instâncias.
Com tecnologia de GPU
Iremos alocar antecipadamente o IndirectParameter Buffer e o Instance Data Buffer com base no número de grupos e no número de instâncias (neste caso, o Instance Data Buffer apenas tem o espaço alocado antecipadamente; os dados efetivos serão preenchidos durante o processo de culling da GPU).
Ao mesmo tempo, calculamos antecipadamente o deslocamento da instância (Instance Offset) de cada grupo e armazenamo-lo no campo InstanceStart do parâmetro, mantendo apenas uma instância do buffer de instâncias durante todo o processo.
Além disso, precisamos de gerar um buffer com informações por objeto (incluindo GroupID, LOD Distance Range, Bounds, Transform, entre outras), para obter as propriedades de cada objeto durante o recorte na GPU.
- Recorte na CPU: Antes do recorte na GPU, realizamos primeiro um recorte aproximado na CPU, para determinar se o grupo, no seu conjunto, está visível.Partindo de uma caixa de envolvimento raiz, comparamos a relação entre o volume total das caixas de envolvimento dos objetos e o volume da caixa de envolvimento resultante da fusão; se esse valor for inferior ao limiar, procedemos à divisão recursiva da caixa de envolvimento (com o objetivo principal de evitar que, devido a uma distância excessiva entre dois objetos, seja gerada uma caixa de envolvimento global de dimensões excessivas).Ao mesmo tempo, combinamos isto com o PVS para avaliar melhor a visibilidade do grupo. Como não dispomos de uma funcionalidade semelhante ao IndirectExecute do DX12, a nossa poda na GPU apenas reduz o número de instâncias, não eliminando as chamadas de desenho (drawcall) do grupo como um todo; por isso, é necessário eliminar, com a maior precisão possível, os grupos totalmente invisíveis através da poda na CPU.
- Recorte por GPU: O recorte por GPU realiza, através de um único despacho, um recorte em três etapas para todos os grupos, objeto a objeto, incluindo recorte de cone de visão, recorte de LOD e eliminação de ocultação Hiz. Através do recorte, o valor de «Instance Count» do parâmetro é incrementado em 1 e os «InstanceData» são emitidos.
- Exclusão de sombras: Seguimos o método partilhado no «Dragon's Dogma», reprojetando a profundidade da imagem para o espaço das sombras como uma «Shadow Reveiver Mask». Se o volume projetado pelo «Shadow Caster» não se cruzar com a máscara, é possível excluí-lo, evitando assim a renderização de sombras desnecessárias.
Além disso, no que diz respeito à secção «Por que razão não implementámos o Cluster/Meshlet», em primeiro lugar, isso implica uma sobrecarga significativa no dispositivo móvel e, em segundo lugar, a implementação do Cluster no GLES também apresenta problemas de compatibilidade. Tendo tudo isto em conta, consideramos que dar prioridade à otimização do desempenho de cada DrawCall nos proporcionará um aumento de desempenho gratuito e direto.
Otimização do desempenho de uma única chamada de desenho (DrawCall)
Em observações anteriores, constatámos que muitas otimizações do tempo de processamento da CPU na renderização tendem a centrar-se excessivamente no número de DrawCalls, negligenciando o tempo de execução de cada DrawCall individualmente. Consideramos que a redução do número de DrawCalls é apenas um método de otimização, sendo que o tempo de processamento final da CPU é o único indicador relevante.
Os dispositivos móveis modernos e os padrões gráficos já são, na verdade, capazes de lidar com um grande número de drawcalls há muito tempo. Esta questão foi também abordada pela equipa do motor HypeHype na SIGGRAPH 2023 — eles testaram 10 000 drawcalls com diferentes malhas e materiais num iPhone 6s, o que demorou apenas 11,27 ms.Outros dispositivos Android equivalentes também conseguem, em geral, manter uma taxa de fotogramas superior a 60. E em 2014, quando o Metal surgiu, foi também apresentado o slogan de que permitiria 10 vezes mais drawcalls do que o GLES.
Hoje, 11 anos depois, a razão pela qual continuamos a debater-nos com o excesso de DrawCalls deve-se principalmente a vários fatores de sobrecarga, incluindo a alternância excessiva entre PSOs, o envio e a cópia de buffers, a lógica de renderização do motor e o excesso de chamadas à interface RHI, que aumentam a carga sobre a CPU.Por isso, consideramos que a otimização do desempenho não deve centrar-se apenas no número de DrawCalls, mas sim ter em conta todos estes fatores de forma integrada.
- Otimização da alternância PSO: depende principalmente do equilíbrio entre o número de variantes de shader e a complexidade dos shaders em cada projeto.A fila de renderização do RenderGroup é ordenada de acordo com a prioridade dos shaders, materiais e malhas; ao mesmo tempo, aplicamos um tratamento especial às sombras: os materiais sem AlphaTest utilizam uniformemente o mesmo shader para renderizar a profundidade das sombras, reduzindo assim a frequência das alternâncias do PSO durante a renderização das sombras.
- Otimização do envio do buffer: no GLES, o mapeamento e desmapeamento do buffer acarretam um custo significativo; embora o mapeamento persistente suportado pelo RHI moderno consiga reduzir significativamente o tempo de upload, ainda não consegue evitar as múltiplas cópias de dados da thread principal para a thread de renderização e, posteriormente, para a memória do buffer, bem como as operações memcmp.Por isso, adotámos as três estratégias específicas a seguir, que reduziram significativamente o tempo de upload do buffer:
- O PerRendererBuffer armazena os parâmetros de cada Renderer (como a luz ambiente SH recebida pelo objeto) num Uniform Buffer gerido pelo objeto Renderer, sendo estes diretamente associados durante a renderização;
- O PerShaderBuffer destina-se a buffers uniformes que não necessitam de alterações por material, sendo submetido apenas uma vez aquando da mudança de shader. Em comparação com o PerRendererBuffer, o PerShaderBuffer é mais flexível, podendo suportar diferentes variantes de shader;
- No que diz respeito ao PerMaterialBuffer, recorremos ao código do SRP Batcher para pré-gerar buffers por material e ligá-los diretamente.
- Otimização da lógica de renderização: para garantir flexibilidade e estabilidade, os motores de jogos comerciais realizam avaliações lógicas complexas durante a renderização. Por exemplo, no motor Unity, sempre que se chama a função `Draw`, é primeiro chamada a função `ApplyMaterial`, que atualiza todos os estados e parâmetros de renderização antes da renderização propriamente dita; quando o número de `DrawCalls` é elevado, isto pode implicar um tempo de processamento considerável. Por isso, implementámos as seguintes otimizações:
- A interface ApplyMaterial foi separada, sendo chamada ativamente pela camada superior apenas quando for necessário alternar entre materiais ou parâmetros;
- Sempre que for necessário alterar o PerMaterialBuffer, utilize a interface específica simplificada.
Após a otimização, o tempo de execução da nossa CPU reduziu em 1/3 para o mesmo número de DrawCalls.
- Otimização das chamadas RHI: O principal objetivo da otimização das chamadas RHI é reduzir as chamadas à API gráfica, com exceção das chamadas «Draw Primitive». As otimizações específicas incluem:
- Agrupar os buffers de vértices e índices com o mesmo stride, evitando a ligação de VB/IB em cada chamada de desenho, o que reduz o tempo de execução em 15%;
- Quando o Resource não sofre alterações, a configuração do DescriptorSet é ignorada, o que reduz ainda mais o tempo de execução em 30%; quando o SetDescriptors, por si só, demora bastante tempo e a mudança de Descriptor aumenta ainda mais o tempo de execução do próximo draw, tal como foi descrito no Guia de Melhores Práticas da Arm.
Testámos o tempo de execução de 5 000 DrawCalls em dispositivos Android de gama baixa. Ao utilizar a renderização nativa do motor, o tempo de execução da thread de renderização foi de 34,79 ms.Após otimizarmos o envio de buffers e a lógica de renderização, o tempo de execução diminuiu para 22,97 ms. Com uma otimização adicional do número de chamadas RHI, o tempo de execução reduziu significativamente para 11,8 ms. Por fim, mantendo inalterado o número de DrawCalls, conseguimos reduzir o tempo de execução da CPU para menos de um terço do valor inicial.
Resultados dos testes de desempenho em cenários de benchmark de outras tentativas de otimização
Também experimentámos algumas novas funcionalidades do RHI, incluindo:
- Multi-Draw Indirect (MDI): permite uma otimização significativa em dispositivos compatíveis, melhorando, em certa medida, o problema em que a eliminação de objetos ocultos pela GPU pode resultar na submissão de chamadas de desenho (DrawCall) vazias (redução do número de submissões na CPU);
- Bindless: No entanto, o desempenho do Bindless não foi o esperado, tendo-se verificado uma misteriosa «otimização negativa» mesmo nos dispositivos Android mais recentes. Ao combinar o MDI com o Bindless, conseguimos renderizar praticamente todos os objetos com um único DrawCall, mas o tempo de processamento da CPU acabou por ser superior ao que se verificava sem agrupamento. Este é também um exemplo negativo de uma preocupação excessiva com o número de DrawCalls.É claro que esperamos que os futuros chips móveis ofereçam um melhor suporte ao Bindless. Nesta fase, tentámos desenvolver, com base no Texture Streaming do Unity, um sistema SVT sem Feedback como alternativa; esta solução ainda se encontra em fase de validação.
De acordo com os resultados dos testes de cenários de benchmark, o RenderGroupRenderer, em comparação com a renderização original sem instanciamento, reduziu as chamadas de desenho (DrawCall) em 1/3,o tempo de execução das threads de renderização diminuiu significativamente em três quartos, e o tempo de execução da thread principal também diminuiu em dois terços (embora o tempo de execução em C# tenha aumentado, o tempo gasto com o recorte nativo do motor e com a atualização do GameObject diminuiu, o que, no geral, ainda resultou numa otimização significativa).
2. Plano de iluminação
Plano de iluminação
- Pipeline de renderização direta:
Optámos por utilizar o pipeline de renderização direta no projeto, tendo em conta os seguintes aspetos: em primeiro lugar, o pipeline direto apresenta vantagens na resposta às necessidades artísticas complexas e variáveis, não sendo necessário preocuparmo-nos se a adição de algumas propriedades de materiais poderá causar um aumento do GBuffer.
Em segundo lugar, as pipelines de atraso tradicionais não são muito favoráveis em termos de largura de banda para plataformas móveis. O OnePassDeferred, por sua vez, apresenta algumas limitações em termos de flexibilidade, como a impossibilidade de alterar as dimensões do RT a meio do RenderPass, nem de recuperar conteúdo de píxeis fora da posição atual.
No GLES, a compatibilidade do FrameBufferFetch também apresenta problemas: os diferentes chips suportam um número variável de RTs de fetch; alguns suportam apenas 1 RT, pelo que é necessário recorrer ao PLS para a sua implementação, mas os nossos testes revelaram que o desempenho do PLS não é o ideal.
Além disso, as quatro fontes de luz por objeto integradas no motor não são suficientes para objetos de maiores dimensões, pelo que experimentámos o Forward+. No entanto, o Forward+ consome demasiado tempo nos equipamentos mais antigos; se limitarmos o número máximo de fontes de luz por mosaico, o número de fontes de luz dentro do mosaico torna-se incontrolável quando a câmara muda de ângulo, e ultrapassar esse limite pode causar erros de desempenho.
Para resolver estes problemas, adotámos uma divisão do espaço mundial horizontal em «tiles» — com uma divisão padrão de 2 metros por «tile», distribuídos à frente da câmara, com um máximo de 4 fontes de luz por «tile» e um mapa de índice de 128×128. Este método de divisão torna a sobreposição das fontes de luz entre «tiles» estável, facilitando a deteção atempada de problemas de excedência de limites durante a produção.
- Melhorias no processo de gestão de versões do Vulkan
Adicionámos um Light Pre-Pass baseado no Subpass ao pipeline das futuras versões do Vulkan.
Na passagem Pre-Z, geramos um GBuffer RT simplificado e guardamo-lo.Uma vez que a nossa iluminação local utiliza um modelo PBR simplificado sem Fresnel, não precisamos de gerar especular ou albedo no GBuffer; basta agrupar a normal, a rugosidade e algumas informações específicas de ID de material ou atributos num único GBuffer RGBA8,depois, podemos executar um processo de renderização de volume de iluminação semelhante ao Deferred Shading, guardando os resultados da iluminação geométrica na Tile Memory.
Posteriormente, na fase de sombreamento (Shading Pass), voltaremos a desenhar o objeto e iremos recolher essas informações de iluminação, combinando-as com as propriedades do material, como o albedo, obtidas durante a renderização, para obter o resultado final de iluminação.
Codificamos o MotionVector necessário para o TAA como RGBA8, em que R + G == 0 representa a ausência de velocidade efetiva; desta forma, certos materiais que não emitem velocidade podem armazenar outras informações no canal BA.
Por exemplo, no caso de vegetação simples e em grande quantidade, guardamos as suas informações UV no canal BA do MotionVector; assim, durante a passagem de sombreamento (Shading Pass), basta, na pós-processamento, obter as informações geométricas do gbuffer e as informações UV do MotionVector para recriar o aspeto do material da vegetação.
O fluxo do pipeline na versão Vulkan é, em linhas gerais, o seguinte: primeiro, o PreZ Pass gera a profundidade (Depth), o GBuffer e o MotionVector; em seguida, é calculada a eliminação de ocultação das sombras;em seguida, é executada a renderização de profundidade das sombras, seguida de cálculos como AO e SSS no espaço do ecrã; depois, entramos no NativeRenderPass, onde, no SubPass, são calculados a ShadowMask e o Light Pre-Pass, além de ser executado o Shading Pass normal. Por fim, saímos do RenderPass e executamos outros passes de pós-processamento.
As melhorias no pipeline de versões do Vulkan também apresentam algumas limitações; por exemplo, o Light Pre-Pass só pode substituir o modelo de iluminação predefinido; para modelos de iluminação que requerem mais canais no Gbuffer, continua a ser necessário utilizar o Forward+.
No entanto, disponibilizamos um parâmetro selecionável por fonte de luz, que permite forçar a utilização do «Standard Lit Model» para uma determinada fonte de luz, aplicando um tratamento uniforme a todos os materiais. Desta forma, é possível fazer com que os píxeis dentro do mesmo «Tile» sejam influenciados por mais de quatro luzes, embora isso implique sacrificar a precisão do «Lighting Model».
- Bom dia
Na parte do GI difuso, adotámos a abordagem mais tradicional de Lightmap + Light Probe, em que o Lightmap armazena apenas informações de luz indireta,já que, no Light Probe, para além do modo normal de um único ponto de amostragem por objeto, disponibilizamos também um modo de múltiplos pontos de amostragem, que permite definir vários pontos de amostragem para cada objeto, com interpolação com base nas coordenadas do centro de gravidade de segmentos, triângulos ou tetraedros.
Nas duas imagens comparativas abaixo, a imagem da esquerda mostra o efeito de um único ponto de amostragem, sendo que a parte inferior da caixa apresenta uma iluminação ambiental uniforme; a imagem da direita, por sua vez, apresenta o resultado da utilização de dois pontos de amostragem, sendo possível observar que os lados esquerdo e direito recebem iluminação indireta diferente.
No que diz respeito à GI especular, baseamo-nos principalmente na «Reflection Probe» com correção AABB. Além disso, para certos tipos específicos de pavimentos ou superfícies de água, também utilizamos agentes de reflexão planos. Em termos gerais, pode ser considerada uma espécie de HLOD destinada especificamente à representação de reflexos.
Além disso, seguindo o exemplo do God of War, normalizámos o CubeMap do Reflection Probe. Mais concretamente, gerámos um coeficiente SH de iluminação ambiental com base nos píxeis do CubeMap e dividimos a cor dos píxeis do CubeMap pela iluminação ambiental nessa direção, obtendo assim um CubeMap normalizado.Na renderização propriamente dita, multiplicamos cada pixel pelo iluminação ambiental real recebida na direção de reflexão, para restaurar a cor refletida.
A vantagem desta abordagem é que, mesmo que um grande número de objetos utilize a mesma sonda de reflexão, os reflexos de diferentes áreas podem produzir diferenças de claridade e escuridão distintas.
3. Otimização das sombras
Conceção de funcionalidades
O desenho básico do nosso sistema de sombras é o seguinte:
- Sombra de grande plano do CSM+ de nível 3 para personagens / POSM para múltiplas personagens: Sombra de grande plano exclusiva do CSM+ de nível 3 da Cascade para personagens de nível 1; em certas cenas com múltiplas personagens, é utilizado o POSM (Per-Object Shadow Map);
- Suporta a projeção de duas luzes cónicas;
- ScreenSpaceShadowMask: todos os resultados das sombras acima são enviados para uma ScreenSpaceShadowMask RGBA8;
- R: Sombra direcional, G: Sombra local 1, B: Sombra local 2, A: AO: O canal R armazena as sombras da luz principal, os canais G e B armazenam as sombras das luzes cónicas e o canal A armazena as informações de AO.
Exclusão por distância
Primeiro, realizámos uma filtragem simples com base na distância: ajustámos os valores de profundidade dos vértices dos triângulos no pós-processamento ScreenSpaceShadow de acordo com a distância da sombra e, em seguida, utilizámos o ZTest Greater para renderizar, eliminando o cálculo das sombras fora da distância da sombra.
Como é necessário amostrar a profundidade ao calcular as sombras, precisamos de duas cópias da profundidade, uma para o «Test» e outra para o «Sample». No «NativeRenderPass», iremos copiar uma cópia do «Depth Buffer» sem memória para o «Test», de modo a evitar, na medida do possível, largura de banda adicional de leitura e escrita.
Detecção da zona de penumbra
Adicionámos a funcionalidade de deteção da zona de penumbra: primeiro, calculamos o PCF com 1/4 da resolução; em seguida, no Shadow Pass de resolução total, amostramos uma máscara de 1/4 e aplicamos o PCF de resolução total apenas aos píxeis cujos valores de sombra se encontram na zona intermédia, reduzindo assim a carga computacional sem comprometer a qualidade do resultado.
Para evitar que, após este processo, surjam problemas de imprecisão na deteção de determinados pixéis, calculamos dois conjuntos de normais com base, respetivamente, na derivada da posição no buffer de 1/4 e nos quatro valores de profundidade do «Gather» em resolução total. Se o ângulo entre as normais for superior ao limiar, considera-se que o pixel de baixa resolução não é fiável e é forçada a execução do PCF em resolução total.
Segue-se a vista de depuração da cena; a área a vermelho foi identificada por nós como a zona de penumbra, sendo que apenas estes píxeis executarão o PCF em resolução total.
Desvio por píxel
Utilizámos o algoritmo «Receiver Plane Depth Bias» para implementar o «Shadow Bias» pixel a pixel. O seu princípio é relativamente simples: em primeiro lugar, aplica-se a regra da cadeia bidimensional às derivadas das coordenadas de sombra no espaço do ecrã, para calcular as derivadas no espaço das sombras.
Utilizando a derivada parcial e o desvio de amostragem do PCF, podemos calcular o valor do desvio. No caso do ponto central, adicionámos o resultado do desvio com um desvio de 1 píxel como desvio inicial.
A imagem seguinte apresenta os resultados da comparação entre o bias fixo e o bias por píxel:
Na imagem da esquerda, que utiliza um valor de polarização fixo, é possível observar uma zona de fuga de luz na parte inferior da caixa, bem como alguma distorção na auto-sombra nas superfícies quase perpendiculares à direção da iluminação; após a aplicação da polarização pixel a pixel (imagem da direita), apenas aumentamos a polarização nas áreas com maior derivada, o que permite resolver o problema da distorção da auto-sombra, mantendo simultaneamente a projeção dos detalhes.
No entanto, quando a profundidade do ecrã não é contínua, o desvio por pixel pode calcular resultados errados, o que leva a alguns fenómenos de fuga de luz. Para resolver este problema, é necessário que o artista especifique manualmente os valores máximo e mínimo do desvio.
Mapa de sombras em cache com deslocamento
Em cenários com um número elevado de DrawCalls, também experimentámos o Scrolling Cached Shadow Map, que inclui, nomeadamente:
- Ao armazenar em cache a profundidade do CSM, para objetos cujos fotogramas anterior e seguinte estejam totalmente contidos no cone de sombra, basta deslocar o CSM do fotograma anterior para a posição de projeção do fotograma atual para obter diretamente a profundidade da sombra, evitando assim a renderização direta do objeto;
- Aplicar o Scrolling apenas ao último nível da cascata; quando o âmbito da cascata é relativamente pequeno, um grande número de objetos irá intersectar-se com o cone de visão, o que limitará o efeito da otimização;
- Atualizar a cache a cada vários fotogramas, para aliviar a pressão sobre a largura de banda.
No futuro, tencionamos também oferecer suporte ao Local ShadowMap Atlas e a mecanismos de cache. Iremos suportar a projeção de mais de duas luzes locais e ajustar dinamicamente a precisão do ShadowDepth de acordo com a proporção da área ocupada pela fonte de luz no ecrã; para fontes de luz locais a longa distância, iremos também introduzir suporte a cache estático.
1. Esquema de iluminação das personagens
No que diz respeito aos esquemas de iluminação de personagens, creio que todos já se depararam, em maior ou menor grau, com os seguintes tipos de problemas:
Ao analisar estas questões, é possível resumir as mesmas nas seguintes três necessidades:
Com base nas necessidades acima referidas, elaborámos um projeto específico de iluminação para as personagens.
A iluminação é composta por 【luz direta】 e 【luz indireta】. Normalmente, temos apenas uma luz paralela — a que costumamos chamar de luz principal. A luz principal ilumina normalmente a cena, mas, ao iluminar a personagem, mantemos a sua direção e sobrescrevemos a cor e a luminosidade da luz principal utilizando um método semelhante a uma caixa de pós-processamento. A forma concreta de o fazer é a seguinte:
- Passar ao Shader mais uma cópia da cor da luz principal da personagem; quando o Shader da personagem obtiver a luz principal, a cor obtida será a cor da luz principal da personagem;
- Forneceu à personagem uma fonte de luz paralela adicional, sem projeção, para servir de luz de contorno;
- Ao mesmo tempo, foram reservadas duas luzes adicionais para as personagens; essas luzes adicionais podem ser qualquer combinação de luzes pontuais e holofotes, capazes de iluminar normalmente as personagens e os objetos do cenário dentro do seu raio de ação (uma vez que cada quadrado de 2 metros pode ter, no máximo, quatro luzes adicionais, foram atribuídas duas luzes às personagens).
No que diz respeito à luz indireta, utilizamos o sistema LightProbe do Unity para criar sondas e implementámos nós próprios a parte responsável por guardar a luz indireta nas sondas, separando as sondas da cena das sondas das personagens em dois conjuntos distintos, que são armazenados e utilizados separadamente;
Para os realces da luz ambiente, utilizamos a mesma sonda de reflexão; no entanto, para alguns materiais específicos, disponibilizamos a opção de introduzir um CubeMap no material para substituir a sonda de reflexão do ambiente.
Guardamos estas informações de iluminação que afetam as personagens num ScriptableObject, que o técnico de iluminação ajusta e, em seguida, guarda como um modelo;A imagem à direita, abaixo, mostra as informações guardadas do esquema de iluminação do personagem, incluindo as duas luzes paralelas mencionadas acima, duas luzes adicionais, o sh guardado pela sonda, bem como algumas informações que podem ser ajustadas adicionalmente nas caixas de pós-processamento e a opção de utilizar ou não sondas de reflexão personalizadas.
Por fim, utilizamos um gestor que funciona de forma semelhante a uma pilha. A escolha deste método de gestão está intimamente relacionada com a utilização específica — normalmente, além de carregar novos esquemas de iluminação, uma das funcionalidades mais utilizadas é a de restaurar o efeito do esquema de iluminação anterior; por isso, adotámos o método de gestão em pilha.
Nesta fase, este esquema já dispõe de funcionalidades como a separação de personagens e cenários, a possibilidade de alternar em tempo real e o suporte à personalização de modelos de iluminação. Por fim, definimos a alternância do esquema de iluminação como um comportamento de evento no editor de enredo, permitindo a ligação de animações de iluminação.
Os efeitos de luz interligáveis são os seguintes:
A imagem abaixo mostra a ferramenta de edição de enredo específica do projeto; basicamente, todos os parâmetros relacionados com a iluminação e as sombras, bem como parte do pós-processamento e dos efeitos físicos, podem ser controlados neste editor de enredo.
2. Grande plano com sombras
A luz e a sombra sempre foram indissociáveis. Tal como mencionado anteriormente, o nosso esquema de sombras consiste num CSM de três níveis, complementado por sombras de grande plano. O princípio de funcionamento consiste em utilizar um osso especificável do personagem como centro de uma esfera de raio definido, utilizando essa esfera para construir e gerar este mapa de sombras.durante o cálculo das sombras no espaço do ecrã, é realizada uma comparação de precisão, utilizando a imagem de sombra com maior precisão entre esta e as sombras em cascata como mapa de sombras para esse píxel.
Como se pode ver na animação seguinte, a personagem estava inicialmente toda dentro da sombra da luz principal; ao ativar a sombra de grande plano, passou a ser iluminada normalmente pela luz principal, precisamente porque a sombra de grande plano alterou o plano de corte próximo;ou seja, a nossa sombra de grande plano é uma imagem de sombra com parâmetros ajustáveis individualmente, incluindo os planos de corte de perto e de longe, a distância máxima, a luz utilizada e o «bias», que costuma ser o que mais dá dores de cabeça.
3. Detalhes da pele
No que diz respeito à pele, vamos centrar-nos em alguns detalhes, tomando como exemplo os efeitos de rubor e de transpiração.
- Efeito de rubor
Em geral, o processo de corar é uma mudança gradual, em que o grau de rubor varia de acordo com a zona do rosto; por exemplo, na maioria das pessoas, quando coram, o rubor começa primeiro nas orelhas, seguindo-se as bochechas, embora, ocasionalmente, todo o rosto fique corado.
Para simular este processo, adotámos as seguintes medidas, de modo a tornar as imagens mais vivas e realistas:
- Máscara desenhada à mão: controla as áreas de rubor, o gradiente de cor e a intensidade com base na textura da máscara;
- Independência multicanal: permite ajustar separadamente o efeito de rubor em diferentes áreas, como o rosto, as orelhas e o nariz;
- Processo de alteração pré-armazenada: o processo de transição do rubor é registado por canal nas curvas correspondentes, permitindo uma expressão emocional natural.
- Efeito de transpiração
O nosso jogo disponibiliza uma funcionalidade de acompanhamento desportivo, em que o protagonista acompanha o jogador em alguns treinos desportivos, pelo que é necessário incluir efeitos de transpiração adequados. A implementação concreta é feita principalmente através dos seguintes três aspetos:
- Combinação de materiais e partículas: o shader de material simula o brilho e a hidratação da superfície da pele; para o efeito das gotas de suor, é possível optar entre a implementação do material aderido à pele e a implementação por efeitos visuais (VFX);
- Máscara para controlar as áreas de transpiração: utilizar uma imagem de máscara para determinar as áreas de transpiração do material, reforçando o realismo e o caráter artístico do efeito de transpiração;
- Transmissão automática de dados: as alterações nos parâmetros principais determinam automaticamente os parâmetros dos materiais e das partículas.
A imagem abaixo ilustra alguns métodos de cálculo específicos e o resultado final.
△ Calcular a posição de formação das gotas de suor e ajustar a rugosidade da posição das gotas de suor
△ Geração de números aleatórios através do ID da grelha UV
△ Simulação da trajetória de uma gota de suor ao cair
△ Efeitos da transpiração com o sistema de acompanhamento de exercício físico
A partilha dos efeitos físicos centrou-se principalmente em quatro aspetos, incluindo a implementação da simulação de tecidos, o controlo de performances em tempo real, o desenvolvimento com base no Unity DOTS e o módulo de deteção de colisões.
1. Implementação da simulação de tecidos
Para resolver alguns problemas específicos do projeto, desenvolvemos internamente um sistema de simulação de tecidos.
Sistema de simulação de tecidos baseado em esqueleto: StrayCloth
O StrayCloth utiliza um método de simulação que combina o XPBD com o substep. Em comparação com o PBD, a vantagem do XPBD é que não depende do número de iterações nem do passo temporal; quando combinado com o substep, pode melhorar significativamente a convergência da resolução.
O que torna isto um pouco especial é o facto de utilizarmos esqueletos como partículas simuladas, o que significa que cada partícula, além da posição, contém também informação sobre a rotação.
Na implementação concreta dos subpassos, adotamos um tempo de subpasso dinâmico, variando entre 1/200 e 1/300, de acordo com os diferentes cenários de exigência de desempenho. Além disso, realizamos uma interpolação de movimento dos objetos em movimento no cenário, o que torna o efeito das colisões mais estável.Na verdade, embora a interpolação de movimento não implique um custo de desempenho muito elevado, a sua aplicação prática revela-se bastante complicada devido à grande variedade de tipos, tais como partículas estáticas, corpos de colisão e campos de vento, entre outros.
Por que razão se utilizam esqueletos em vez de malhas proxy? Principalmente pelas seguintes três razões:
- O «Love and Deep Space» apresenta requisitos complexos em termos de enredo, combate e troca de trajes; o esquema de esqueleto permite uma transição suave entre animações e cálculos;
- Tendo em conta as necessidades de controlabilidade e as limitações de desempenho nas plataformas móveis, a abordagem baseada em esqueletos proporciona aos artistas uma maior margem de manobra;
- Utilizando esqueletos e restrições, é possível construir estruturas semelhantes a malhas para obter um efeito semelhante.
Esquema de restrições esqueléticas
Nas soluções existentes para a simulação de tecidos com esqueleto, as restrições do esqueleto são frequentemente implementadas com base em restrições de forma locais e globais. Embora sejam simples e rápidas, apresentam uma desvantagem evidente: quando utilizadas na simulação de tecidos, o resultado tende a ter um estilo cartoon, o que não corresponde ao estilo realista 3D pretendido por «Love and Deep Space»;Além disso, o ajuste dos parâmetros não é intuitivo, uma vez que existem dois parâmetros de rigidez de curvatura — global e local —, o que dificulta o ajuste artístico e a harmonização dos efeitos em diferentes cenários.
Por conseguinte, no que diz respeito ao esquema de restrições esqueléticas, optámos por uma restrição esquelética baseada na haste de Cosserat. As suas vantagens incluem:
- O resultado é mais natural e está mais em sintonia com o estilo artístico realista geral de «Love and Deep Space»
- O ajuste dos parâmetros é mais intuitivo e a resistência nos três eixos é separada; em algumas situações, como na simulação de uma saia, é possível aproximar o efeito de um espartilho através da resistência à flexão anisotrópica.
- É possível reutilizá-lo diretamente na simulação do cabelo, pelo que o cabelo e a roupa também podem partilhar um conjunto de restrições.
Para conhecer os resultados concretos, consulte o desempenho do cartão diário mais recente:
A ligação entre o tecido e a personagem
A ligação entre o tecido e a personagem é feita principalmente de duas formas:
- Hierarquia: a estrutura óssea estática é diretamente afetada pela animação óssea da personagem e move-se de acordo com a relação hierárquica.
- Este método é relativamente simples e apresenta bons resultados em pontos de ligação que tendem a ser rígidos. No entanto, em locais mais complexos, onde a junção entre ossos é influenciada por vários ossos ou onde existe um certo grau de alongamento e contração — como, por exemplo, no cotovelo, no ombro e na cintura —, é comum que o tecido se separe da personagem.
- Adesão: as partículas estáticas são controladas pelos triângulos de ancoragem do modelo de personagem. A malha é pré-calculada em paralelo e atualizada a cada fotograma através das coordenadas do centro de gravidade.
No caso específico de degeneração do triângulo, utilizamos a transformação dos ossos de skinning dos vértices do triângulo para efetuar um equilíbrio ponderado e atualizar a transformação das partículas estáticas.
Esquema de colisão
No que diz respeito ao esquema de colisões, utilizamos um BVH dinâmico para gerir a fase ampla (broad phase) das colisões na cena, sendo que cada personagem funciona como uma subárvore que contém os seus corpos de colisão internos como nós da subárvore.
Ao mesmo tempo, através do ID da personagem, da visibilidade partilhada e do tipo de componente, estas três regras permitem-nos gerir as regras de colisão partilhadas entre diferentes personagens e diferentes componentes.
Na fase «narrow», não geramos contacto diretamente, mas sim armazenamos em cache os pares de corpos de colisão, para os resolvermos de forma mais específica na subetapa; graças às vantagens da subetapa que adotamos, na maioria dos casos basta utilizar diretamente o DCD para evitar alguns problemas de penetração causados por movimentos rápidos, sem necessidade de introduzir operações como o CCD ou o contacto preditivo.
- Implementação do Mesh Collider
No caso de corpos de colisão geométricos parametrizados, como «plane», «capsule» e «box», é relativamente simples resolver as colisões entre estes e as partículas, bem como com as arestas. Em áreas complexas, como os ombros, o pescoço, o peito e as costas, os corpos geométricos parametrizados têm dificuldade em representar com precisão a forma do modelo da personagem, o que pode facilmente causar efeitos de penetração; por isso, nessas áreas, recorremos amplamente ao «Mesh collider».
No entanto, como o «mesh collider» é um corpo côncavo irregular, por vezes pode não ser fechado,o que torna mais difícil alcançar um efeito de colisão preciso em comparação com a geometria parametrizada, especialmente em dispositivos móveis. Por isso, adotamos o hash como método de pesquisa aproximada de triângulos, combinando-o com os resultados dos triângulos vizinhos armazenados em cache, para gerar um par de colisão partícula-triângulo antes do início da iteração,nas iterações subsequentes, verificamos se a partícula se encontra dentro do perímetro do triângulo; caso ultrapasse esse perímetro, limitamos a busca a um intervalo de passos com base nas relações de vizinhança dos triângulos do modelo, para obter o triângulo mais próximo, e armazenamos o resultado na cache para utilização na próxima iteração.
A animação abaixo apresenta alguns exemplos concretos do desempenho do projeto; como se pode ver, o desempenho é relativamente estável.
Face Collider
O colisor facial pode ser considerado um colisor de malha especial; em comparação com o colisor de malha básico, tem uma forma mais fixa e mais suave, e, a partir do centro do modelo, praticamente não há sobreposição de triângulos; por isso, utilizamos um CubeMap de 16×16 para pré-calcular os triângulos em todas as direções,para que, durante o cálculo da colisão, seja possível localizar rapidamente os triângulos adjacentes.
Colisão entre camadas
A simulação de colisões entre tecidos no jogo é a parte mais difícil de tratar; por razões de desempenho, a solução que propomos é a seguinte:
- Utilização do hashing espacial como estrutura de aceleração de pesquisas
- Com a pré-divisão em camadas feita pelo departamento de artes, considera-se apenas a colisão entre partículas e triângulos entre camadas
- Para evitar que os objetos fiquem presos entre camadas, calcular apenas as colisões entre partículas e triângulos na direção da normal única
O departamento de artes plásticas realiza previamente a divisão do tecido em camadas, considerando apenas as colisões entre essas camadas. Utiliza-se um hash como estrutura de aceleração de pesquisa e, para evitar situações de encravamento entre as camadas, consideramos apenas as colisões numa única direção normal; caso já tenha ocorrido penetração, ignora-se a colisão, deixando a correção a cargo das etapas seguintes.
Na prática, utilizamos a posição das partículas do subpasso anterior para verificar a colisão com a posição atual das partículas, o que permite desacoplar os dados de forma simples e evitar dependências.
Separação por penetração entre camadas
No que diz respeito às partes em que a colisão entre camadas já resultou em penetração, inspirámo-nos no método de «untangling cloth» e utilizámos uma solução leve: através da divisão do tecido em camadas, partindo dos pontos de fixação do tecido, calculámos os pontos de intersecção entre as arestas e os triângulos das diferentes camadas. Uma vez que a estrutura dos nossos recursos é necessariamente uma malha uniforme,é possível deduzir de forma relativamente simples, através dos pontos de intersecção da malha, os triângulos de outras partículas. Por fim, aplicamos restrições elásticas aos pares partícula-triângulo que se encontram em penetração para resolver a penetração. Na prática, devido à relação com os subpassos, a probabilidade de penetração é relativamente baixa; por isso, adotamos uma execução por fotogramas e por blocos para aliviar a pressão sobre o desempenho.
2. Controlo de atuações em tempo real
A maior parte dos efeitos físicos apresentados na narrativa de «Love and the Deep Space» é concretizada através do controlo e ajuste dos vários efeitos físicos nas cenas cinematográficas. A nossa equipa de ferramentas desenvolveu e mantém um conjunto de ferramentas de cenas cinematográficas extremamente poderosas e, com base nelas, desenvolvemos várias faixas de funcionalidades para regular especificamente os efeitos físicos.
Aqui temos um exemplo da «Cutscene Physics Track» do nosso motor de animação. Como os nossos colegas de arte são muito exigentes no que diz respeito à representação visual, é possível ver que a configuração de toda a trajetória física é bastante complexa.
△Exemplo de pista de física para cenas cinematográficas
SmoothBlendPose Track
Durante a interpretação, um problema muito comum é a instabilidade física causada pelas transições instantâneas de movimentos, que ocorrem frequentemente tanto nas cenas narrativas como nas mudanças de roupa.
Desenvolvemos um método relativamente versátil que, ao registar a postura física inicial, realiza um cálculo de interpolação entre a postura inicial e a postura atual durante a transição, o que permite reduzir significativamente a oscilação,É claro que isto acarreta algum custo em termos de tempo, geralmente da ordem das dezenas de milissegundos, o que, na maioria dos casos, é aceitável. São fornecidos alguns parâmetros, tais como o número de iterações de interpolação e o tamanho do passo de interpolação, para que os artistas possam ajustar de acordo com as necessidades reais.
Pose Track
É claro que o SmoothBlendPose tem as suas limitações e não garante uma transição totalmente suave, especialmente em cortes de cena complexos durante a representação de cenas narrativas.Também disponibilizamos uma solução mais direta: guardar offline o estado físico de um determinado fotograma e, durante a reprodução, aplicar diretamente esse estado físico ao tecido, o que permite evitar na perfeição os problemas decorrentes das mudanças de plano.
Editar Faixa de Parâmetros
É difícil que um único ativo físico consiga satisfazer as diferentes necessidades de representação nas várias cenas do enredo; por exemplo, por vezes pretende-se que o tecido seja mais ou menos macio, ou que a amortecimento seja maior ou menor.Disponibilizamos uma linha temporal para a edição de parâmetros, que permite editar e modificar os parâmetros em tempo real. A grande maioria dos parâmetros pode ser substituída, o que facilita muito a realização de alterações num intervalo de fotogramas específico. Esta modificação de parâmetros também pode ser utilizada para criar efeitos especiais, como, por exemplo, o efeito de ruptura em imagens animadas, obtido através da edição dos parâmetros de restrição.
Faixa de Animação
Os efeitos físicos por si só não são, na verdade, suficientes para sustentar todos os aspetos da representação da cena; muitas vezes, é necessária a combinação de animação e física para criar interações. Utilizamos as faixas de animação para estabelecer a ligação e a integração entre a animação e a física, controlando com precisão o comportamento em diferentes intervalos de fotogramas.No fluxo de trabalho prático, existe uma diferença considerável entre o comportamento dos movimentos no DCC e a sua representação final no motor; isto inclui situações em que, após alterações no sistema de rig em tempo real do motor, a animação pode «atravessar» outros elementos. Por isso, com base na integração da animação, podemos sobrepor efeitos de colisão física para evitar essas sobreposições.
A imagem animada mostra os efeitos de interação entre a física e a animação do colar, incluindo a transição do estado físico para o estado animado, bem como a transição entre diferentes animações.
Pista de Colisão e Pista de Vento
O Collider Track e o Wind Track permitem criar e eliminar dinamicamente corpos de colisão e campos de vento nas cenas cinematográficas. É possível alterar de forma flexível o estado dos corpos de colisão e dos campos de vento, de acordo com as diferentes necessidades da cena. Através do agrupamento por personagens, tipos de componentes e camadas de tecido, é possível controlar com precisão o âmbito dos objetos a serem afetados.
Além disso, é possível adicionar controlo por fotogramas de animação à grande maioria dos parâmetros dos corpos de colisão e das trajetórias do campo de vento, incluindo a forma e o tamanho dos corpos de colisão, a direção, o alcance, a intensidade e a turbulência do campo de vento, o que facilita aos artistas o controlo dos efeitos físicos e a regulação precisa das alterações.
A animação apresenta alguns exemplos de como se apresentam a cápsula ferroviária e o campo eólico.
3. Desenvolvimento com base no Unity DOTS
Empregos + Burst + Matemática
O conjunto de ferramentas DOTS é extremamente poderoso, permitindo o desenvolvimento multithread de alto desempenho diretamente na camada C#. O nosso sistema físico foi totalmente construído na camada C# utilizando o DOTS, o que facilita bastante as iterações de funcionalidades e a depuração. Atualmente, conseguimos suportar simulações com mais de 2000 partículas esqueléticas.
É claro que também realizámos algumas otimizações específicas no projeto, com vista a melhorar ainda mais o desempenho.
Cache de tarefas
Na simulação, o número de tarefas e as relações de dependência estão definidos, e os dados das tarefas não se alteram com frequência. Normalmente, dentro de um fotograma, um conjunto de tarefas com o mesmo número e as mesmas relações de dependência é executado em vários ciclos. No Unity, as tarefas têm de ser recriadas de cada vez que são iniciadas; embora seja possível aliviar esta situação iniciando as tarefas antecipadamente, isso continua a bloquear o fio principal. Além disso, após a conclusão da execução, as tarefas têm de ser limpas.Com base nas observações acima, desenvolvemos uma solução de «Cache Job», que consiste em criar antecipadamente os dados do job e reutilizá-los em cada execução, evitando assim a perda de desempenho resultante da recriação do job a cada vez.
Na prática, é relativamente simples, uma vez que se trata de uma estrutura dedicada, que considera apenas alguns cenários de utilização fixos. Foi adicionada uma Atomic Queue para armazenar tarefas de cache, utilizando a técnica «fetch and add array» para armazenar as tarefas específicas. À direita, encontra-se um diagrama esquemático do fluxo de execução das tarefas de cache pelos workers.
Intrínsecas do Néon
O Burst gera código SIMD de alto desempenho para diferentes plataformas, que pode ser facilmente visualizado no Burst Inspector. Após análise no Burst Inspector e testes em dispositivos reais, em certas situações também é possível melhorar ainda mais o desempenho através da escrita manual de intrínsecas Arm Neon.
Apresenta-se aqui um exemplo da implementação para verificar se um vetor contém algum elemento maior que 0.
Dot(float4)
No que diz respeito à multiplicação escalar, apresento aqui três métodos; a utilização de funções intrínsecas Neon permite obter um aumento de desempenho de cerca de 30% em comparação com as funções matemáticas, nos casos de teste.Se o modelo alvo for compatível com o ARMv8.2, é possível utilizar as novas instruções de adição por convenção para melhorar ainda mais o desempenho. De um modo geral, a maioria dos modelos populares atualmente no mercado é compatível com o ARMv8.2.
Transpose(float4x4)
No que diz respeito aos cálculos de transposição, verifica-se que o código de montagem gerado pelo `mathematics` apresenta um desempenho muito baixo; ao escrever manualmente as funções intrínsecas NEON, é possível obter um enorme aumento de desempenho.
Se se tratar apenas de uma transposição pura e simples, pode-se utilizar diretamente a leitura entrelaçada; a implementação aqui apresentada deve-se ao facto de, na minha prática habitual, transformar o produto escalar numa multiplicação vetorial através da transposição de 4 float4.
Aqui apresentam-se apenas exemplos mais comuns destes dois projetos. Uma vez que o código de matemática é geralmente incorporado, é necessário realizar otimizações específicas de acordo com o contexto do código; para tal, podem ser utilizados o Burst Inspector e testes em dispositivos reais para realizar testes de desempenho específicos.
4. Módulo de deteção de colisões
Por que razão se optou por desenvolver de novo, em vez de utilizar o módulo de física já consolidado do Unity?
O Unity possui, por si só, um conjunto de módulos de física maduros baseados no PhysX; no entanto, a decisão de desenvolver novos módulos de física fora do âmbito do Unity deve-se principalmente às seguintes considerações:
- O «Love and Deep Space» apresenta uma grande variedade de modos de jogo, cujas configurações de camadas são relativamente independentes entre si; gostaria muito que fosse possível manter um conjunto de configurações de camadas para cada um deles.
- Alguns módulos, como o de combate, requerem mecanismos especiais de ativação e desativação, que se pretende que sejam suportados já ao nível da camada de base; além disso, pretende-se também um controlo mais flexível do fluxo de execução.
- Por último, temos também algumas ideias no que diz respeito à exploração do desempenho: será que, nos casos em que apenas é necessária a deteção de colisões, é possível melhorar o desempenho utilizando o DOTS?
A implementação em «Love and the Deep Sky» inclui:
- Foram implementadas, na sua totalidade, todas as funcionalidades nativas de deteção de colisões
- Lógica personalizada de atualizações e gatilhos
- Interface de consulta segura para threads, que pode ser chamada sem problemas pelas camadas superiores
- Graças à estrutura leve do DOTS, é possível obter um aumento de até cerca de 15 % nos testes de desempenho
Exemplo de processo de consulta
Uma vez que, no equipamento real, o número efetivo de threads é fixo em 4, o alocador de memória pode ser pré-configurado de acordo com o número de threads, podendo a memória ser obtida diretamente com base no índice da thread atual no momento da alocação.
Utilizando um BVH dinâmico baseado em SAH como estrutura de aceleração da fase ampla, é efetuado um equilíbrio de rotação nos nós de vários níveis adjacentes ao nó da operação atual durante operações de inserção, eliminação e movimentos fora do intervalo.
Uma vez que os objetivos da deteção de colisões são relativamente gerais e os requisitos de precisão não são tão elevados, sacrificámos um pouco da precisão para simplificar alguns algoritmos de deteção de colisões, com vista a melhorar o desempenho.
Exemplo de processo de acionamento
Para satisfazer os requisitos do módulo de combate, concebemos uma lógica de ativação especial para os «triggers»: os eventos «Enter» e «Exit» de um «trigger» têm de ocorrer em pares. Como se pode observar no diagrama de fluxo abaixo, ao remover «b» da função em que «a» ativa «b», são ativados todos os «colliders» que se sobrepõem a «b»,Isto difere da lógica nativa do Unity — na lógica nativa do Unity, a remoção de b na lógica do trigger não aciona os triggers de outros coliders. Por fim, utilizamos o contador «History» para marcar a versão do colider, resolvendo assim alguns problemas potenciais que a lógica de reutilização poderia causar.
Seja bem-vindo à nossa equipa,
Juntos, vamos criar mais coisas boas através da tecnologia!
原创文章,作者:游茶妹儿,禁止转载:https://youxichaguan.com/pt/archives/213131
