Shader OSL Multithread

Julho 24, 2011

Essas últimas semanas eu estava tentando implementar suporte a múltiplas threads do shader OSL. O BRL-CAD usa pthreads para paralelizar o trabalho.

A primeira vez que eu pus o shader OSL pra rodar, tive problemas, provavelmente devido à leitura e escritas simultâneas a dados na memória. Eu tinha adiado esse problema e estava trabalhando apenas com uma thread.

O que a minha função osl_render faz é preencher uma estrutura chamada RenderInfo com dados como ponto de interseção, a normal, um ponteiro para o shader dessa superfície, etc. Preenchida essa estrutura, ela faz uma chamada ao método QueryColor da classe OSLRender. O objeto dessa classe é global, o que significa que está em uma posição de memória compartilhada pelas threads. Como a chamada do método QueryColor modifica o objeto, temos o problema do acesso simultâneo a esse método.

Primeira tentativa

A primeira coisa que eu tentei, foi proteger a chamada dessa função com semáforos. O BRL-CAD tem uma biblioteca interna de utilitários, entre eles a implementação de um semáforo.

Basicamente, o que eu fiz foi o seguinte:

OSLRender oslr;
...
bu_semaphore_acquire(BU_SEM_SYSCALL);
oslr.QueryColor();
bu_semaphore_release(BU_SEM_SYSCALL);

BU_SEM_SYSCALL na verdade é uma macro representando um inteiro. Quando uma thread chama a função bu semaphore acquire(BU_SEM_SYSCALL), ela bloqueia o acesso a outras threads para o mesmo inteiro.

Com isso o código passou a funcionar para mais de uma thread! Porém, fui conservador demais ao bloquear a chamada do QueryColor. Creio que 90% do trabalho da função osl_render seja feito lá. Logo, colocando um semáforo nessa função, a paralelização vai por água abaixo. De fato, fiz testes com 1 e 2 processadores e os tempos para renderizar uma imagem de teste foram exatamente os mesmos.

Segunda tentativa

Fui estudar um pouco mais o código do OSL e descobri que ele tem suporte a aplicativos multithread. Basicamente, cada thread deve manter uma estrutura local chamada ThreadInfo. Ao chamar o sistema de shaders do OSL, passamos essa informação, que será modificada.

Gambiarra!

Para manter uma ThreadInfo para cada thread, precisamos identificá-las de alguma forma. O problema é que não temos acsso a essa informação diretamente. O único jeito que encontrei sem ter que mudar a estrutura interna do raytracer do BRL-CAD foi a seguinte: toda vez que a função osl_render é chamada, uma estrutura chamada struct application é passada. Essa estrutura tem um campo chamado resource, que é um ponteiro para um pedaço de memória exclusivo para cada thread.

Esse pedaço é alocado na criação das threads e não muda ao longo de suas vidas. Assim, usar o endereço desse pedaço de memória é um jeito (feio) de identificá-las. Outro problema é que, embora haja uma função de inicialização do shader (chamado osl_setup), as threads são criadas após isso, o que me obriga a inicializá-las na função osl_render mesmo.

A solução na qual cheguei foi a seguinte:

/* Every time a thread reaches osl_render for the 
   first time, we save the address of their own
   buffers, which is an ugly way to identify them */
std::vector<struct resource *> visited_addrs;
/* Holds information about the context necessary to
   correctly execute a shader */
std::vector<void *> thread_infos;

int osl_render(struct application *ap, ...){
    ...
    bu_semaphore_acquire(BU_SEM_SYSCALL);
    
    /* Check if it is the first time this thread is
       calling this function */
    bool visited = false;
    for(size_t i = 0; i < visited_addrs.size(); i++){
        if(ap->a_resource == visited_addrs[i]){
            visited = true;
            thread_info = thread_infos[i];
            break;
        }
    } 
    if(!visited){
        visited_addrs.push_back(ap->a_resource);
        /* Get thread specific information from 
           OSLRender system */
        thread_info = oslr->CreateThreadInfo();
        thread_infos.push_back(thread_info);
    }
    bu_semaphore_release(BU_SEM_SYSCALL);
    ...
}

Classe thread-safe

Com as ThreadInfo's, podemos nos despreocupar com o acesso concorrente ao objeto da classe OSLRender. Para ter uma garantia de que não será feita escrita nesse objeto, podemos declarar os métodos como const, como no exemplo abaixo:

struct A {
    int x;
    // OK
    void set_x(int _x) { x = _x; };
    // ERRO, tentando modificar membro do objeto 
    // num método const
    void const_set_x(int _x) const { x = _x; };
};

Testes

Com essa nova implementação, os tempos melhoraram bastante com mais processadores. A tabela abaixo mostra os tempos de execução para renderizar uma cena de exemplo, usando até 4 processadores.

Gráfico 1: Tempo x no. de processadores

Os ganhos foram praticamente lineares, sendo que para 4 processadores a medida de paralelização foi de 1.25 (se fosse perfeitamente linear seria 1.0).

Próximos passsos

Falei com meu mentor sobre o desenvolvimento daquele modo de visualização incremental, mas parece que ele não gostou muito da ideia. Isso exigiria uma mudança bastante grande no sistema, pois o renderizador do BRL-CAD foi desenvolvido para ray-tracing e não path-tracing.

Por enquanto estou estudando maneiras de adaptar o código OSL para suportar ray-tracing, mas não sei se isso é viável. A propósito, eu ainda confundo bastante os termos ray-tracing, path-tracing, photon mapping e todos esses algoritmos de iluminação e pretendo em breve escrever um post cobrindo esses tópicos bem por cima.

Anúncios

O shader OSL

Julho 10, 2011

Nessa última semana consegui avançar de maneira satisfatória no projeto. Primeiramente, implementei o shader sh_osl que é chamado pelo aplicativo rt e usa os serviços do sistema do OSL.

No post anterior mencionei que o sistema OSL exigia muitos raios por pixel e isso seria um problema ao usar o rt, mas descobri que uma opção de hypersampling que faz exatamente isso.

De maneira simplista, temos então que o aplicativo rt atira raios através da função rt_shootray e que chama uma callback chamada shadeview toda vez que um objeto é atingido. A função shadeview chama outra callback que está associada ao objeto e corresponde ao shader dele.

Por exemplo, se o objeto atingido tem o shader sh_glass, então esse objeto possui um ponteiro para a função glass_render. O que a função shadeview faz é chamar a função referenciada por esse ponteiro.

O que a função glass_render ou qualquer outra função xyz_render deve fazer é essencialmente retornar a cor do ponto de consulta (passado como parâmetro). Vou descrever então como implementei a função osl_render, correspondendo ao shader sh_osl.

Reflexão

Primeiramente, implementei a reflexão através de uma chamada recursiva da função rt_shootray, que atira um novo raio em uma dada direção. Essa parte foi fácil pois o código que eu tinha desenvolvido para o raytracer independente que eu havia implementado fazia do mesmo jeito.

A figura a seguir é a renderização da cena da caixa de cornell usando um shader osl de espelho para a caixa maior e um shader brl-cad de xadrez para a caixa menor.


Figura 1: Teste de integração de um shader OSL e um shader BRL-CAD

No final das contas não tive que tomar nenhum cuidado adicional ao misturar shaders OSL e BRL-CAD, ao contrário do que eu havia dito no post anterior.

Refração

Implementar a refração foi mais complicado. Primeiramente, descobri que o detector de colisões do BRL-CAD sempre calcula dois pontos de interseção para cada superfície. Um, chamado inhit, é o ponto da superfície no qual o raio bate inicialmente. O outro, chamado outhit, supõe que o raio foi atirado de dentro da superfície.

A função osl_render já é chamada com o ponto de interseção P equivalente a inhit, pois quem seta isso é o shadeview. Entretanto quando o raio é de refração (interno) eu quero que P seja o outhit.

Portanto, tive que escrever uma nova callback para tratar raios refratados. Sempre que um raio for retornado pelo OSL, verifico se ele é refratado. Para isso, basta fazer um produto escalar entre a normal e o novo raio para ver se eles apontam em direções “opostas”. Em caso positivo, mudo a callback que será chamada a próxima vez que um objeto for atingido no rt_shootray. O que essa callback faz é setar P como outhit e chamar xyz_render.


Figura 2: Teste com o shader vidro

Implementando novos shaders para o OSL

Para verificar se a interface sh_osl está computando os dados corretamente, decidi criar novos shaders que utilizam esses dados. Um exemplo é o shader xadrez, que usa as coordenadas do mapeamento uv.

Aproveitei para implementar suporte aos grupos de shaders, comentado em um post anterior, além da possibilidade de setar os parâmetros via a interface mged.

Agora é possível definir um grupo de shaders através da própria interface. O grupo de shaders é então construído a partir de shaders primitivos. Por exemplo a descrição do shader

shadername gen_color#layername#c1#base#color#1.0#0.0#1.0
shadername gen_color#layername#c2#base#color#0.0#1.0#0.0
shadername checker#layername#chk#K#float#4.0
join c1#Cout#chk#Cw
join c2#Cout#chk#Cb

foi usada no caixa menor, como ilustra a imagem abaixo:


Figura 3: Teste com shader xadrez verde-magenta

A sintaxe ficou meio feia mas está funcional. A primeira linha descreve um shader de cor genérica inicializado com a cor magenta enquanto o da segunda linha possui a cor verde. O terceiro shader descrito é o shader xadrez propriamente dito, que usa a saída de dois outros shader’s para compor a cor dos quadrados.

A quarta e quinta linhas tratam de ligar a saída do primeiro e segundo shader à entrada do shader xadrez.

A vantagem de descrever shaders dessa maneira, é que se por exemplo eu quiser compor o shader xadrez com uma cor verde e com um shader de espelho é só mudar para

shadername mirror#layername#c1
shadername gen_color#layername#c2#base#color#0.0#1.0#0.0
shadername checker#layername#chk#K#float#4.0
join c1#Cout#chk#Cw
join c2#Cout#chk#Cb

Que o resultado será:


Figura 4: Teste com shader xadrez verde-espelho

Além do mais, a parede do fundo da Figura 3 usa o shader “nuvem” adaptado de um shader BRL-CAD. Não parece nem um pouco com nuvem, mas é uma textura procedural, então não dá pra fazer milagre.

Próximos passos

Preciso resolver a questão do multi-threading. Semana que vem pretendo estudar como funcionam as funções de aquisição e liberação de recursos providas pelo BRL-CAD e qual recurso do sistema OSL eu preciso garantir acesso exclusivo.

Mais pra frente gostaria de implementar um modo de visualização incremental. Atualmente se quisermos visualizar a imagem enquanto ela é renderizada, temos que esperar todas as amostragens serem feitas para cada pixel antes de vê-lo. Minha ideia é que a cada amostragem, toda a imagem seja atualizada, de forma que inicialmente vejamos um monte de pixels dispersos e conforme mais e mais amostragens sejam feitas, a imagem vá convergindo para uma sem ruídos.

Idealmente, gostaria também de implementar uma interface para fazer a composição do grupo de shaders OSL. A GUI do BRL-CAD é escrita em Tcl, linguagem que eu teria que estudar antes de mais nada. Creio que não conseguirei fazer isso antes do término do programa, mas pretendo fazer isso algum dia.


Raytracer OSL usando a estrutura BRL-CAD

Junho 26, 2011

Faz algum tempo que eu não posto sobre meu projeto do BRL-CAD, mas é porque eu não vinha tendo nenhum resultado interessante para mostrar. A maior parte do tempo eu gastei resolvendo aspectos mais técnicos de programação, sobre os quais escrevi nas últimas semanas.

Interface OSL BRL-CAD

Comecei escrevendo uma interface para o renderizador OSL. Essa interface recebe os dados necessários sobre o ponto sendo renderizado. Esses dados incluem: as coordenadas do ponto P sendo renderizado, o nome do shader da superfície a qual P pertence, a normal dessa superfície em P e a direção do raio de incidência em P.

Essa modularização permite que o modo como o renderizador OSL calcula a cor nesse ponto seja transparente à aplicação. Por outro lado, o renderizador não precisa saber como os objetos da cena são manipulados e como as interseções são calculadas.

Um problema é que o renderizador OSL pode decidir que o raio será refletido (por exemplo se o shader não for totalmente opaco). Como ele não sabe nada sobre a cena sendo renderizada, ele precisa devolver o trabalho para a aplicação. Por isso, ele retorna uma estrutura dizendo se houve reflexão e qual a direção do raio refletido.

Com esse novo raio, a aplicação fará seus cálculos e chamará a interface novamente.

Testando a interface

Antes de partir para a implementação do shader osl, decidi testar a nova interface com uma adaptação daquele raytracer sobre o qual falei em um post anterior.

Usando a mesma cena, consegui reproduzir as mesmas imagens.

Conversando com um dos meus mentores, me foi sugerido então tentar renderizar uma cena modelada no BRL-CAD usando apenas shaders osl.

Cornell box na interface do mged.

A cena é conhecida por caixa de cornell. Para determinar as interseções de um raio com um objeto, usei a função rt_shootray provida pelo BRL-CAD. Para ela, devemos passar a origem e direção do raio além de uma callback que será chamada sempre que um raio for atingido. Me baseei nesse exemplo.

Para testar, fiz as paredes e o teto serem um azul difuso, o chão ser vermelho difuso, a caixa alta um amarelo difuso e a caixa mais baixa um espelho. O resultado ficou o seguinte, com 400 amostragens:

Cena renderizada com 400 amostragens.

Apesar de a cena ter ficado meio escura, gostei do resultado.

Próximos passos

Minha ideia agora é adaptar esse código para o shader osl. Já andei fazendo alguns testes e a tarefa não parece simples. Um problema é que o aplicativo rt, que usará o shader osl, só atira um raio por pixel, enquanto o código acima usa vários.

Isso é necessário porque a direção do raio de saída de um shader osl é probabilística e é preciso uma grande amostragem de raios para ter a cor mais próxima do esperado.

Para se ter uma ideia, veja a cena da caixa de cornell renderizada com um número baixo de amostragens:

Cena renderizada com 4 amostragens.

Outro problema com o qual terei que lidar é a mistura de shaders do BRL-CAD com os shaders OSL. O mecanismo de funcionamento deles é meio diferente e terei que estudá-los mais a fundo para fazer uma eventual adaptação.

No mais, fiquei mais tranquilo de ter conseguido implementar um renderizador independente, pois isso se mostra um projeto mais concreto no qual eu posso continuar trabalhando e apresentar no final, caso a implementação do shader osl não dê resultados.

Meu medo era de ficar enroscado com algum problema e por isso o projeto não ser bem sucedido.

Nota finais

Ganhei acesso de commit ao código-fonte do BRL-CAD. É bastante satisfatório poder contribuir diretamente com um código grande, que é usado por muitas pessoas.

A regra da comunidade é fazer commits constantemente, sempre que o código estiver estável. Por enquanto só fiz atualizações do meu programa e uma correção de erros de digitação que encontrei perdidos no código.

O BRL-CAD possui uma página no ohloh, onde dá pra ver os contribuidores e os commits que foram feitos.


Shaders para o BRL-CAD

Maio 29, 2011

Como eu já havia dito anteriormente, o BRL-CAD é um software de modelagem através de geometria construtiva sólida. Essa modelagem consiste em, dado um conjunto qualquer de primitivas básicas como cubo, esfera, cilindo, etc., construir objetos mais complexos baseando-se em operadores tais como união, interseção e diferença.

A vantagem desse tipo de modelagem é que a descrição do objeto fica bem compacta, bastando guardar os parâmetros das primitivas e as operações feitas sobre elas. A desvantagem é que os objetos que podem ser construídos dessa forma são bem limitados.


Exemplo de objeto construído a partir de cones, esferas e cubos.

Renderizando um objeto no BRL-CAD

Uma das ferramentas do BRL-CAD é chamada de mged. Ela consiste de um terminal e uma janela de visualização. Nesse terminal, o usuário pode digitar comandos para criação de objetos primitivos, realizar operações entre objetos, etc. Os objetos criados podem ser visualizados na outra janela. A figura abaixo destaca o wireframe de uma taça criada através de comandos no terminal.

Screenshot da interface do programa mged.

Depois de modelado, é possível renderizar a cena através de raytracing. A figura abaixo é resultado da renderização com uma textura padrão, o phong shader (modela a textura de plástico).

Objeto renderizado usando o shader de plástico.

Pela própria interface mged dá para atribuir shaders para o objeto. Alguns deles são: plástico, espelho, vidro, xadrez, nuvem, luz, etc. Pelo pouco que experimentei esses shaders não são muito realistas (ou eu não estou usando direito :P). Abaixo segue uma imagem aplicando o shader de vidro à taça, sobre um plano laranja, com uma fonte de luz e com background preto.

Teste com shader de vidro.

Estudando os shaders do BRL-CAD

Agora que já aprendi como testar um shader visualmente, é hora de estudar o código de um deles. O mais simples é o null shader, simplesmente porque ele não faz nada! Ele serve para termos uma ideia das interfaces que devem ser implementadas. A maior parte do BRL-CAD, incluindo seus shaders, é escrita em C.

struct mfuncs null_mfuncs[] = {
    {MF_MAGIC, "null", 0, MFI_HIT, 0,
     sh_null_setup, sh_null_render,
     sh_null_print, sh_null_free },

    {MF_MAGIC, "invisible", 0, MFI_HIT, 0,
     sh_null_setup, sh_null_render,
     sh_null_print, sh_null_free },    

    {0, (char *)0, 0, 0, 0, 0, 0, 0, 0}
};

Cada shader deve preencher um array de estruturas como as do código acima, e este array deve ser terminado pela estrutura nula. Cada estrutura dessa representa uma interface para o programa principal. O segundo parâmetro (preenchido com “null” e “invisible” no exemplo) representa o nome pelo qual o shader será referenciado.

A função sh_null_setup será chamada no começo do programa e deve inicializar o shader; A função sh_null_render é chamada cada vez que um raio do raytracer bater em uma superfície com esse shader e deve preencher a cor no ponto de interseção; A função sh_null_print é chamada sempre que um erro acontece e deve imprimir uma mensagem de debug; Finalmente, sh_null_free libera eventual memória alocada.

Se fosse em C++, poderíamos definir uma classe base para mfuncs e cada elemento do array acima poderia ser uma classe derivada dela. Note como sh_null_setup é bem o que um construtor costuma fazer e sh_null_free faz as vezes do destrutor.

Partindo para um shader não trivial, fui olhar o código do Toon Shader que gera imagens não foto-realistas, com aparência de desenho animado, conforme mostra a figura abaixo:


Teste com o shader ‘toon’

Para entender seu funcionamento, temos que falar sobre a lei dos cossenos de Lambert:

Quanto maior o ângulo entre a luz e a normal no ponto sendo avaliado, menor intensidade de luz difusa será refletida. Isso pode ser notado pelos gradientes na imagem da taça de plástico. O que o toon shader faz é discretizar esse gradiente, dividindo-o em buckets. Note que na figura da taça com shader toon, é possível ver as divisões entre as diferentes tonalidades de cinza.

Desenvolvendo um novo shader para o BRL-CAD

Minha tarefa era desenvolver um shader do tipo polka dot ou do tipo zebra. Optei pelo polka dot, cuja textura lembra aqueles vestidos de bolinha:

Pesquisando um pouco sobre como implementar tal shader, descobri um escrito para o RenderMan. Nele há duas variáveis s e t que não foram passadas como parâmetro, então imagino que sejam globais.

Apesar dos nomes s e t, pareceu ter relação com UV mapping . Eu já havia ouvido falar disso quando mexia com o Blender. A ideia é mapear a superfície de um objeto 3D em um plano 2D. Um exemplo clássico desse mapeamento é a projeção do globo terrestre em mapas 2D.

Da mesma forma que mapas, há diversas maneiras de se fazer o mapeamento e nenhuma delas é necessariamente melhor. No BRL-CAD o mapeamento é feito de maneira automática pelo sistema de shaders. Um shader já existente é a da textura Xadrez. Renderizei a imagem da taça com esse shader:


Teste com shader xadrez.

O sistema de shaders do BRL-CAD fornece as coordenas u e v correspondente à projeção no plano. O que eu fiz foi substituir essas cordenadas pelo s e t do shader do RenderMan. O resultado ficou o seguinte:

Teste com shader ‘polka dot’.

A shader não ficou nada bom. As bolinhas estão distorcidas. Porém, meus mentores do projeto disseram que é o suficiente. O importante era que eu aprendesse o básico da criação dos shaders. Como acessar as variáveis globais, como compilar, etc.

Próximos Passos

Conversando com os mentores, decidimos que o próximo passo será desenvolver um shader para interfacear com o sistema de shaders do OSL. Creio que com conhecimento adquirido estudando o raytracer usando OSL e desenvolvendo um shader para o BRL-CAD, dá para realizar essa tarefa.


Shaders escritos em OSL

Maio 22, 2011

Finalmente descobri o que estava fazendo a imagem ser renderizada de maneira errada, conforme mencionado no post anterior. Tenho até vergonha de admitir, mas eu estava usando a versão antiga do shader que modela vidro.

Implementação de um novo shader

Um dos shaders mais simples que existe é o que modela uma superfície perfeitamente áspera, ou seja, só tem reflexão difusa. A descrição em OSL de uma tal superfície é dada por:

surface yellow () {
    Ci = color(1.0, 1.0, 0)*diffuse(N);
}

Apliquei esse shader na esfera da direita, resultando na seguinte imagem:

Grupos de Shaders

Para testar o grupo de shaders, criei 3 shaders iguais ao yellow.osl, mas com as cores vermelha, verde e azul, chamando-os de red.osl, green.osl e blue.osl, respectivamente. Também criei um shader chamado blender.osl, que recebe como parâmetro três cores closure e faz uma média delas:

surface blender (
	closure color r = diffuse(N),
	closure color g = diffuse(N),
	closure color b = diffuse(N)){
	Ci = r*(1.0/3) + g*(1.0/3) + b*(1.0/3);
}

Na hora de criar o shader group, declarei cada shader e conectei a saída da cores no blender. Diferentemente da especificação, a implementação do sistema de shaders atual do OSL, ao criar um shader, passamos como parâmetro o uso do shader (superfície, deslocamento, etc.), o nome do shader e o nome do layer. A ligação é feita conforme a especificação:

shadingsys->ShaderGroupBegin();
// Declaracao dos shader layers
shadingsys->Shader("surface", "red", "red1");
shadingsys->Shader("surface", "green", "green1");
shadingsys->Shader("surface", "blue", "blue1");
shadingsys->Shader("surface", "blender", "blender1");
// Ligação entre os shaders
shadingsys->ConnectShaders("red1", "Cout", "blender1", "r");
shadingsys->ConnectShaders("green1", "Cout", "blender1", "g");
shadingsys->ConnectShaders("blue1", "Cout", "blender1", "b");
shadingsys->ShaderGroupEnd();

Pelo meu entendimento, o closure resultante seria igual a (1.0/3, 1.0/3, 1.0/3), pois está fazendo a média entre vermelho (1,0,0), verde (0,1,0) e azul (0,0,1), ou seja, a cor seria cinza.

Aplicando na mesma esfera da direita, qual não foi minha surpresa ao vê-la com a cor marrom:

Revisei o código do raytracer e entendi direito o funcionamento. Não é que se calcula a cor resultante a partir das operações do closure. O que se faz é ir descendo na árvore de expressões e calculando a cor. Ao chegar em uma folha, teremos uma dada cor. Se por acaso a primitiva correspondente for escolhida, assim também será a cor.

No exemplo, começa-se a avaliação da árvore com w = (1, 1, 1). Ao descer para a folha de alguma das primitivas, multiplicamos w por 1.0/3, então temos (1.0/3, 1.0/3, 1.0/3). Se a primitiva no shader red.osl for escolhida, por exemplo, multiplicamos ainda por (1.0, 0, 0), resultando em (1.0/3, 0, 0).

Isso significa que na prática, o closure resultante retorna vermelho, verde ou azul com igual probabilidade. Se olharmos a imagem amplificada, dá pra ver que a esfera é composta por vários pixels vermelhos, verdes e azuis misturados.

Próximos passos

Com isso acho que cumpri as tarefas para o período antes do começo do projeto (fase do community bounding). Deixando um pouco o OSL de lado e voltando ao BRL-CAD, as próximas tarefas para serem cumpridas nas próximas duas semanas são:

  • Implementar um shader novo para o BRL-CAD (sugestões incluem texturas de zebra ou de pontos-polka).
  • Atribuir a um objeto e renderizar.

Open Shading Language

Maio 15, 2011

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


Google Summer of Code 2011

Maio 1, 2011

Nessa Segunda-feira saiu o resultado de que meu projeto foi selecionado no Google Summer of Code 2011!

Enviei duas propostas para a organização BRL-CAD. Essa organização desenvolve um programa de modelagem de sólidos através de geometria construtiva sólida. Além do modelador, o software possui outras ferramentas, sendo a principal delas um raytracer para gerar imagens (2D) a partir dos sólidos (3D).

A minha primeira proposta consistia em portar aplicativos independentes já existentes de processamento de imagem, para formar uma biblioteca. Essa é uma tarefa essencialmente de refatoração. Não há muitas dificuldades, mas é um projeto trabalhoso.

A segunda proposta, a que foi aceita, era para melhorar o sistema de shaders do BRL-CAD. Bem por cima, podemos dizer que um shader define a textura do objeto. Podemos implementar shaders para representar vários tipos de materiais como vidro, nuvem, fogo, etc.

Um shader se relaciona diretamente com um raytracer. O raytracer gera imagens 2D a partir de imagens 3D, simulando o comportamento da luz. Imagine uma fonte de luz como uma lâmpada por exemplo. Ela emite infinitos raios de luz em várias direções. Alguns desses raios irão bater em objetos, outros virão diretamente para nosso olho e outros vão para outras direções. Os raios que batem em objetos serão refletidos/refratados em maior ou menor intensidade (por exemplo, um raio incidente a um pano preto quase não será refletido; em uma superfície metálica quase todo o raio que chega será refletido; em uma jarra de vidro o raio será parte refratado e parte refletido). A imagem que formamos do objeto é resultado dos raios que batem nesse objeto e vão para nosso olho.

Essa é uma descrição bem simplificada do comportamento da luz. Imagine que queremos simular esse comportamento computacionalmente. Para cada fonte de luz teríamos que emitir muitíssimos raios de luz e simular o caminho que ele faria. Porém, é certo que a maior parte desse raios jamais chegariam a nossos olhos (ou chegaria com uma intensidade desprezível), o que seria um desperdício computacional enorme.

A sacada é fazer o contrário! Fazer com que a luz siga o caminho inverso, começando a partir dos olhos! Simulamos o caminho percorrido por um raio e ao chegar em uma fonte de luz, saberemos qual é a cor desse raio. Dessa forma apenas simulamos raios que efetivamente chegam no olho.

Cena produzida através de ray tracing

Aonde entram os shaders nessa história? Vimos que os shaders definem as propriedades do material. Em geral, cada superfície está associada a um shader. Quando um raio bate nessa superfície, o shader definirá quanto do raio será refletido, quanto será refratado, qual a cor da superfície, etc. de acordo com o material/textura que ele representa. Porém, a quantidade de raios traçados é bem menor.

Atualmente os shaders do BRL-CAD são implementados em C e compilados como bibliotecas dinâmicas. O raytracer carrega dinamicamente as bibliotecas que serão necessárias para renderizar a cena. A ideia principal da minha proposta é implementar um novo sistema de shaders que possibilite o uso de shaders escritos em OSL.


Basicamente, a OSL é uma linguagem para escrever shaders. O código foi desenvolvido inicialmente pela Sony, que o tornou público em 2010. O diferencial da linguagem é que ela é desenvolvida especialmente para raytracers, de modo que ela emprega técnicas que prometem melhorar o desempenho desses algoritmos. O pacote vem com um compilador para shaders escritos em OSL (formato .osl) e são convertidos para o formato .oso. Além disso, vem um sistema de shaders que manipula os shaders.

Conclusão

O projeto em si é bastante desafiador visto que até um mês atrás nunca tinha ouvido falar de shaders. Além do mais, tenho que conciliar esse projeto com o final do meu mestrado, de forma que até 15 de Agosto terei que me esforçar bastante.

Estou bastante animado com esse projeto especialmente porque envolve o estudo de raytracing, sobre o qual tenho um grande interesse. Idealmente eu gostaria de terminar esse projeto e me envolver com outros projetos do BRL-CAD (ou outras organizações como o Yafaray) envolvendo ray tracers.

Vou focar os posts do blog nesse projeto, aproveitando para escrever sobre seu andamento. Espero que dê tudo certo!