Open Shading Language

A primeira etapa do meu projeto consiste em estudar a Open Shading Language (OSL). Aqui farei uma pequena introdução a essa linguagem, de acordo com o que eu já estudei até agora. Minhas principais fontes de informação são o texto introdutório [1], a especificação da linguagem [2] e o próprio código-fonte [3].

No começo de 2010 a Sony tornou público o código da OSL. A linguagem possui alguns features que a torna atraente para ser usada em conjunto com raytracers. A principal delas parece ser o conceito de closures. Em cada ponto de uma superfície, uma equação pode ter que ser resolvida diversas vezes para calcular a cor que um raio de luz com uma dada direção terá. A ideia é que uma closure armazena a equação sem resolver para as variáveis. Aí basta resolvê-la para uma dada direção de entrada para obter a cor desejada. A ideia é que esse sistema seja mais eficiente.

Especificação da linguagem

A especificação da linguagem é boa: detalhada e com exemplos. Um shader bastante simples está escrito em OSL a seguir:

shader gamma (
        color Cin = 1,
        float gam = 1,
        output color Cout = 1
    )
{
    Cout = pow(Cin, 1/gam);
}

Note que ele é praticamente uma função, que recebe alguns parâmetros de entrada e de saída (que são precedidos pela keyword output). Podemos abstrair a implementação desse shader considerando apenas a funcionalidade. Assim, teremos uma caixa-preta, conforme o seguinte diagrama:

Podemos criar redes complexas de shaders a partir de shaders individuais mais simples, chamadas de shader groups, como o exemplo da figura abaixo:

Cada bloquinho da rede acima é dito um shader layer. Para criar essa rede, basta declarar os shaders e depois declarar suas conexões. Para o exemplo da figura acima:

ShaderGroupBegin();
/* Instanciação dos shaders */ 
Shader("texturemap", /* Nome do shader */
       "tex1",	/* Nome do layer (instância) */
       "string name", "rings.tx"); /* Parâmetros de entrada */ 
Shader ("texturemap", "tex2", "string name", "grain.tx");
Shader ("gamma", "gam1", "float gam", 2.2);
Shader ("gamma", "gam2", "float gam", 1);
Shader ("wood", "wood1");
/* Conexões */
ConnectShaders ("tex1", "Cout", "gam1", "Cin");
ConnectShaders ("tex2", "Cout", "gam2", "Cin");
ConnectShaders ("gam1", "Cout", "wood1", "rings");
ConnectShaders ("gam2", "Cout", "wood1", "grain")
ShaderGroupEnd();

Ray tracing

Depois de ler a especificação da linguagem, começou a parte difícil! Há pouca documentação sobre como utilizar a OSL, ficando restrita ao próprio código fonte. A primeira etapa, compilar, já foi complicado. Escrevi até um post descrevendo as adaptações que tive que fazer para conseguir compilar no meu sistema.

Existe uma lista de email de desenvolvedores do OSL. O problema é que é difícil conseguir ajuda lá, pois dificilmente alguém responde alguma coisa.

Fuçando nos arquivos dessa lista, encontrei uma implementação de um ray tracer que usa OSL, desenvolvido por Erich Ocean e um tal de Brecht Van Lommel, baseado no código do smallpt de Kevin Beason.

A cena utilizada é fixa, com 5 esferas com texturas variadas. A figura a seguir é uma imagem renderizada dessa cena, com 100 amostragens.

A cena contém 9 esferas. Uma delas forma a bola de aparência metálica; outra a bola de vidro. As paredes são modeladas com 4 esferas gigantes, de modo que sua porção visível na cena aparente ser plana. A luz no teto também é uma esfera, só que a textura dela é do tipo emissiva.

Note que a luz na esfera de vidro não está correta. Aparentemente era um problema com a normal, o próprio Brecht mandou um patch com a correção, mas quando eu apliquei ao meu código, obtive a seguinte imagem:

A imagem correta deveria ser a seguinte:

Infelizmente, ainda não descobri o motivo do erro, mas estou investigando…

Estudo do código

Uma boa maneira de me familiarizar com a linguagem OSL e seu sistema de shaders é brincar um pouco com esse raytracer. O modo como o algoritmo gera uma imagem a partir de uma cena 3D pode ser simplificado conforme a figura a seguir:


Esquema básico do funcionamento de um raytracer.

A pirâmide de base retangular com o topo cortado é conhecida como frustum. O que está denotado por near cut plane na imagem, é a imagem 2D de saída.

Vamos descrever os passos do algoritmo em alto nível:

  1. Carregar shaders na memória;
  2. Ajustar os parâmetros da câmera (tais como a posição no espaço e a direção na qual ela está olhando);
  3. Para cada pixel da imagem, devemos obter a cor dele;
  4. Liberar a memória de tudo que foi alocado;
  5. Exportar a imagem.

Obviamente, o passo 3 é a parte principal do algoritmo. Primeramente, note que apesar de pequeno, um pixel ainda é uma representação discreta da realidade. Há infinitos raios de luz que podem passar por ele em diferentes direções. O que se faz em geral é traçar s raios com direções aleatórias (Monte Carlo sampling), mas dentro da região do pixel, onde s é o número de amostragens.

Para a obtenção da cor, temos o seguinte procedimento (radiance):

  1. Identificar qual o primeiro objeto que o raio atinge, bem como o ponto de interseção;
  2. Obter a closure no ponto de interseção;
  3. Avaliar a árvore de expressões do closure;
  4. Extrair a cor do closure.

O passo 1 é geometria. Como estamos lidando apenas com esferas, é fácil computar os pontos de interseção do raio com elas e então encontrar o primeiro objeto atingido.

O passo 2 é trivial, já que a cada esfera está associado um único closure, correspondente ao shader dela.

Uma expressão de closures é composta por operadores de soma e multiplicação por um escalar/cor. Os operandos são chamados de primitivas. O que o avaliador da árvore de expressões faz no passo 3 é, começando com a cor branca (1.0, 1.0, 1.0), calcular qual a cor resultante após as somas e multiplicações da expressão.

Ele também seleciona uma das folhas da árvore (primitivas) de modo aleatório. Uma primitiva pode ser do tipo BSDF (Bidirectional Scattering Distribution Function), emissiva ou background.

A cor retornada pelo passo 3 é a cor do objeto. O porém é que dependendo do tipo de material, a cor que é vista depende da direção do raio de que incide no objeto. Dado isso, o passo 4 determina a cor vista de fato, dependendo do vetor normal e do raio de incidência.

Além do mais, devemos considerar a refração e reflexão, criando um novo raio e chamando a função radiance recursivamente. A cor retornada é então multiplicada pela cor do objeto.

Próximos passos

Segundo o cronograma que montei para o GSoC, devo terminar de estudar a OSL até 23 de Maio. Semana que vem então procurarei fazer duas coisas:

  • Implementar um shader usando a OSL (possivelmente um shader existente no BRL-CAD)
  • Descobrir o erro por trás da imagem com a esfera preta. A princípio vou baixar a versão do código com data próxima àquela em que o Brecht publicou o raytracer. Se funcionar vou verificar as diferenças entre aquela versão e a atual.

Referências:

[1] OSL Introduction
[2] OSL Spec
[3] OSL source code

Os comentários estão fechados.