Este artigo é parte dois de nossa série H - Imersão . A primeira parte pode ser lida aqui: Imersão em Imersão .Ao criar uma animação de apenas 64 KB, é difícil usar imagens prontas. Não podemos armazená-los da maneira tradicional, porque não é eficiente o suficiente, mesmo se você aplicar compactação, por exemplo, JPEG. Uma solução alternativa é a geração procedural, ou seja, escrever código que descreva a criação de imagens durante a execução do programa. Nossa implementação desta solução foi um gerador de textura - uma parte fundamental de nossa cadeia de ferramentas. Neste post, explicaremos como o desenvolvemos e o usamos na
imersão em
H.Os holofotes submarinos iluminam os detalhes do fundo do mar.Versão inicial
A geração de textura foi um dos primeiros elementos de nossa base de código: as texturas processuais já foram usadas em nossa primeira introdução à
B - Incubação .
O código consistia em um conjunto de funções que preenchem, filtram, transformam e combinam texturas, além de um loop grande que ignora todas as texturas. Essas funções foram escritas em C ++ puro, mas a interação da API C foi adicionada posteriormente para que possam ser avaliadas pelo intérprete C
PicoC . Naquele momento, usamos o PicoC para reduzir o tempo gasto em cada iteração: dessa forma, conseguimos alterar e recarregar as texturas durante a execução do programa. Mudar para o subconjunto C foi um pequeno sacrifício em comparação ao fato de que agora poderíamos alterar o código e ver o resultado imediatamente, sem nos preocupar em fechar, recompilar e recarregar a demonstração inteira.
Usando um padrão simples, um pouco de ruído e deformação, podemos obter uma textura de madeira estilizada.Nesta cena da oficina
de F - Felix,
foram utilizadas várias texturas de madeira.Por algum tempo, exploramos os recursos desse gerador e, como resultado, o publicamos em um servidor da web com um pequeno script PHP e uma interface simples da web. Poderíamos escrever o código da textura em um campo de texto e o script o passou para o gerador, que despejou o resultado como um arquivo PNG para exibir na página. Logo, começamos a desenhar no trabalho durante o intervalo do almoço e a compartilhar nossas pequenas obras-primas com outros membros do grupo. Essa interação nos motivou ao processo criativo.
Galeria da Web do nosso antigo gerador de texturas. Todas as texturas podem ser editadas no navegador.Redesign completo
Durante muito tempo, o gerador de textura permaneceu quase inalterado; nós pensamos que era bom, e nossa eficácia parou de aumentar. Porém, uma vez que descobrimos que há muitos
artistas nos fóruns da Internet demonstrando suas texturas totalmente processadas, além de organizar
desafios em vários tópicos. O conteúdo dos procedimentos já foi um recurso da cena demo, mas o
Allegorithmic ,
ShaderToy e ferramentas similares o tornaram acessível ao público em geral. Nós não prestamos atenção a isso, e eles começaram a nos colocar facilmente nas omoplatas. Inaceitável!
Sofá de tecido . Uma textura de tecido totalmente processual criada no Substance Designer. Postado por: Imanol Delgado. www.artstation.com/imanoldelgadoAssoalho da floresta . Textura de solo de floresta totalmente processual criada pelo Substance Designer. Postado por Daniel Thiger. www.artstation.com/deteHá muito que precisamos repensar nossas ferramentas. Felizmente, muitos anos de trabalho com o mesmo gerador de textura nos permitiram reconhecer suas deficiências. Além disso, nosso gerador de malha nascente também nos disse como deveria ser o pipeline de conteúdo processual.
O erro arquitetônico mais importante foi a implementação da geração como um conjunto de operações com objetos de textura. Do ponto de vista de uma perspectiva de alto nível, essa pode ser a abordagem correta, mas do ponto de vista da implementação, funções como
texture.DoSomething () ou
Combine (textureA, textureB) apresentam sérias desvantagens.
Primeiro, o estilo OOP exige que você declare essas funções como parte da API, por mais simples que sejam. Esse é um problema sério, porque não escala bem e, mais importante, cria atritos desnecessários no processo criativo. Não queremos alterar a API toda vez que precisamos experimentar algo novo. Isso complica a experimentação e limita a liberdade criativa.
Em segundo lugar, em termos de desempenho, essa abordagem exige que você processe dados de textura em ciclos quantas vezes houver operações. Isso não seria especialmente importante se essas operações fossem caras com relação ao custo de acessar grandes fragmentos de memória, mas geralmente não é assim. Com exceção de uma fração muito pequena de operações, por exemplo, gerando
ruído ou
preenchimento Perlin , elas são basicamente muito simples e requerem apenas algumas instruções sobre o ponto de textura. Ou seja, contornamos os dados de textura para executar operações triviais, o que é extremamente ineficiente do ponto de vista do cache.
A nova estrutura resolve esses problemas através da reorganização da lógica. A maioria das funções na prática executa independentemente a mesma operação para cada elemento de textura. Portanto, em vez de escrever uma função
texture.DoSomething () que ignora todos os elementos, podemos escrever
texture.ApplyFunction (f) , onde
f (elemento) funciona apenas para um único elemento de textura. Então
f (elemento) pode ser escrito de acordo com uma textura específica.
Parece uma pequena mudança. No entanto, essa estrutura simplifica a API, torna o código de geração mais flexível e expressivo, mais amigável ao cache e permite o processamento paralelo com facilidade. Muitos dos leitores já perceberam que isso é essencialmente um sombreador. No entanto, a implementação real permanece o código C ++ executado no processador. Ainda mantemos a capacidade de executar operações fora do loop, mas usamos essa opção somente quando necessário, por exemplo, por convolução.
Foi:
Tornou-se:
Paralelização
A geração de textura leva tempo, e um candidato óbvio para reduzir esse tempo é a execução paralela de código. No mínimo, você pode aprender a gerar várias texturas ao mesmo tempo. Foi exatamente isso que fizemos
na oficina da F - Felix , e isso reduziu bastante o tempo de carregamento.
No entanto, isso não economiza tempo onde é mais necessário. Ainda leva muito tempo para gerar uma textura. Isso se aplica à alteração, pois continuamos a recarregar a textura várias vezes antes de cada modificação. Em vez disso, é melhor paralelizar o código interno de geração de textura. Como agora o código consiste essencialmente de uma grande função aplicada em um loop a cada texel, a paralelização se torna simples e eficiente. Reduz o custo de experimentos, ajustes e rascunhos, o que afeta diretamente o processo criativo.
Ilustração de uma idéia que exploramos e descartamos para a imersão em H : uma decoração em mosaico com revestimento em orichalcon. Aqui é mostrado em nossa ferramenta de edição interativa.Geração lateral da GPU
Se isso ainda não for óbvio, direi que a geração de textura é realizada completamente na CPU. Talvez alguns de vocês estejam lendo essas linhas agora e estejam perplexos "mas por quê?!". Parece que o passo óbvio é a geração de textura no processador de vídeo. Para começar, aumentará a taxa de geração em uma ordem de magnitude. Então, por que não usamos?
A principal razão é que o objetivo do nosso pequeno reprojeto era permanecer na CPU. Mudar para uma GPU significaria muito mais trabalho. Teríamos que resolver problemas adicionais para os quais ainda não temos experiência suficiente. Trabalhando com a CPU, temos uma compreensão clara do que queremos e sabemos como corrigir erros anteriores.
No entanto, a boa notícia é que, graças à nova estrutura, experimentar a GPU agora parece bastante trivial. Testar combinações de ambos os tipos de processadores será um experimento interessante para o futuro.
Geração de textura e sombreamento fisicamente preciso
Outra limitação do design antigo era que a textura era considerada apenas como uma imagem RGB. Se precisássemos gerar mais informações, digamos que a textura difusa e a textura das normais para a mesma superfície, nada nos impediu de fazer isso, mas a API não ajudou muito. Isso se tornou especialmente importante no contexto do sombreamento fisicamente baseado (PBR).
Em um pipeline tradicional sem PBR, geralmente são usadas texturas coloridas, nas quais muitas informações são armazenadas. Tais texturas geralmente representam a aparência final da superfície: elas já têm um certo volume, as rachaduras são escurecidas e pode até haver reflexões sobre elas. Se várias texturas são usadas ao mesmo tempo, os detalhes de grande e pequena escala são geralmente combinados para adicionar mapas normais ou refletividade da superfície.
Os transportadores PBR de superfície normalmente usam vários conjuntos de texturas que representam valores físicos, em vez do resultado artístico desejado. A textura difusa da cor, que é mais próxima do que costuma ser chamado de "cor" da superfície, geralmente é plana e desinteressante. A cor especular é determinada pelo índice de refração da superfície. A maioria dos detalhes e variabilidade são extraídos das texturas de normais e rugosidade (rugosidade) (que alguém pode considerar o mesmo, mas com duas escalas diferentes). A refletividade percebida de uma superfície se torna uma conseqüência do seu nível de rugosidade. Nesta fase, será mais lógico pensar em termos de não materiais, mas materiais.









A nova estrutura nos permite declarar formatos arbitrários de pixel para texturas. Depois de fazer parte da API, permitimos que ele lide com todo o código padrão. Depois de declarar o formato do pixel, podemos nos concentrar no código do criativo sem gastar muito esforço no processamento desses dados. Em tempo de execução, ele gera várias texturas e as transfere de forma transparente para a GPU.
Em algumas tubulações de PBR, cores difusas e especulares não são transmitidas diretamente. Em vez disso, são usados os parâmetros "cor de base" e "metalicidade", que têm suas vantagens e desvantagens. Em
H - imersão, usamos o modelo difuso + especular, e o material geralmente consiste em cinco camadas:
- Cor difusa (RGB; 0: Vantablack ; 1: neve fresca ).
- Cor especular (RGB: fração da luz refletida em 90 °, também conhecida como F0 ou R0 ).
- Rugosidade (A; 0: perfeitamente lisa; 1: semelhante a borracha).
- Normal (XYZ; vetor unitário).
- Elevação do terreno (A; usado para o mapeamento de oclusão de paralaxe).
Quando usadas, as informações de emissão de luz foram adicionadas diretamente ao shader. Não achamos necessário haver oclusão ambiental, porque na maioria das cenas não há iluminação ambiente. No entanto, não ficarei surpreso que tenhamos camadas adicionais ou outros tipos de informações, por exemplo, anisotropia ou opacidade.
As imagens acima mostram um experimento recente com a geração de oclusão ambiental local com base na altitude. Para cada direção, percorremos uma distância predeterminada e mantemos a maior inclinação (diferença de altura dividida pela distância). Depois calculamos a oclusão a partir da inclinação média.
Restrições e trabalhos futuros
Como você pode ver, a nova estrutura tornou-se uma grande melhoria em relação à antiga. Além disso, ela incentiva a expressão criativa. No entanto, ela ainda tem limitações que queremos eliminar no futuro.
Por exemplo, embora não tenha havido problemas nesta introdução, percebemos que a alocação de memória pode ser um obstáculo. Ao gerar texturas, uma matriz de valores flutuantes é usada. Com texturas grandes com várias camadas, você pode encontrar rapidamente um problema com a alocação de memória. Existem várias maneiras de resolvê-lo, mas todas elas têm suas desvantagens. Por exemplo, podemos gerar texturas lado a lado, enquanto a escalabilidade será melhor, no entanto, a implementação de algumas operações, como convolução, se torna menos óbvia.
Além disso, neste artigo, apesar da palavra "materiais" usados, falamos apenas sobre texturas, mas não sobre shaders. No entanto, o uso de materiais também deve levar a shaders. Essa contradição reflete as limitações da estrutura existente: geração de textura e sombreamento são duas partes separadas, separadas por uma ponte. Tentamos facilitar a travessia dessa ponte, mas na verdade queremos que essas partes se tornem uma. Por exemplo, se um material tiver parâmetros estáticos e dinâmicos, queremos descrevê-los em um só lugar. Esse é um tópico complexo e ainda não sabemos se existe uma boa solução, mas não vamos nos antecipar.
Um experimento para criar uma textura de tecido semelhante ao trabalho de Imadol Delgado mostrado acima.