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.

Anúncios

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


Callbacks e uma solução em C++

Maio 8, 2011

O resolvedor de PLI Xpress possui algumas callbacks que são chamadas em pontos específicos do código. Existem callbacks que são chamadas cada vez que a relaxação linear é resolvida em um nó do branch-and-bound, logo depois de decidir qual variável sofrerá branching, quando encontra uma solução inteira, etc.

As callbacks nada mais são do que ponteiros para funções providas pelo usuário. Por exemplo, a callback que será chamada logo depois de uma variável ser escolhida para sofrer branching possui a seguinte forma:

void (*fucb)(XPRSprob my_prob, void *my_object, int *entity, int *up, double *estdeg)

Ou seja, precisamos passar uma função que recebe my_prob, que é um ponteiro para a estrutura do modelo atual; my_object é um ponteiro para um objeto fornecido pelo usuário; entity é o identificador da variável que vai sofrer branch (geralmente é o número da coluna do modelo); up indica se a variável será arredondada para cima ou para baixo; já estdeg (estimated degradation) indica quanto a função objetivo vai piorar ao fazer o arredondamento.

Para setar essa callback, devemos chamar a seguinte função da API do XPRESS:

int XPRSsetcbchgbranch(XPRSprob prob, void (*fucb)(XPRSprob my_prob, void *my_object, int *entity, int *up, double *estdeg), void *object);

Note que é aqui que devemos informar o endereço da estrutura que será passada para nossa função através de my_object.

Vamos a um exemplo minimal.

void observer(XPRSprob my_prob, 
              void *my_object, 
              int *entity,
              int *up, 
              double *estdeg){
    printf("%d\n", *my_object);
}
...
int obj = 3;
int XPRSsetcbchgbranch(prob, observer, &obj);

Aqui eu passo a variável obj ao setar a callback. Logo, toda vez que a função for chamada, ela imprimirá 3.

E se eu quiser fazer algo mais complicado, como por exemplo imprimir o nome da variável? Eu poderia definir um mapa que leva um índice de coluna no nome da variável.

Porém, é mais eficiente fazer esse mapeamento através de uma função. Minha variável tem o nome x(i,j) e a coluna é dada por col = i*n + j, onde n é o número de variáveis j. Para fazer o mapeamento reverso eu faço: i = col/n e j = col%n.

A função teria a seguinte cara:

void rev_map(int col, int *i, int *j){
    *i = col/n;
    *j = col%n;
}

A função foo a seguir simula a função de callback, que receberá um elemento *void, que nesse caso é um ponteiro para função.

int foo(void *obj){
    void (*ptr)(int col, int *i, int *j)
        = (void(*)(int col, int *i, int *j)) obj;
    int i, j;
    
    ptr(5, &i, &j);
    cout << "i: " << i << ", j: " << j << endl;
}

int main (){
    foo((void*) rev_map);
}

O único porém é que a função depende da variável n. Como fazer? Poderíamos fazer igual à API do XPRESS, colocando um parâmetro a mais em rev_map, e passando um parâmetro a mais para foo.

void rev_map(int col, int *i, int *j, void *ext){
    int *n = (int*)(ext);
    *i = col/(*n);
    *j = col%(*n);
}

int foo(void *obj, void *ext){
    void (*ptr)(int col, int *i, int *j, void *ext)
        = (void(*)(int col, int *i, int *j, void *ext)) obj;
    int i, j;
    
    ptr(5, &i, &j, ext);
    std::cout << "i: " << i << ", j: " << j << std::endl;
}

int main (){
    int n = 3;
    foo((void*) rev_map, &n);
}

O negócio começa a ficar feio…

Podemos usar uma classe para simplificar.

class rev_map {
    int n;
public:
    rev_map(int _n) : n(_n) {};
    void operator() (int col, int *i, int *j){
        *i = col / n;
        *j = col % n;
    }
};

A vantagem é que ela fornece uma função com estado interno (o membro n). Aí não precisa ficar passando o n como parâmetro pra lá e pra cá. Como essa classe é só um wrapper para uma função, achei mais conveniente fazer a sobrecarga do operator () do que definir um método. Esse tipo de classe também é conhecida como functor.

Objetos dessa classe são quase como funções, já que você pode invocá-los diretamente como no exemplo:

rev_map obj;
obj(col, &i, &j);

O resto do código fica:

int foo(void *obj){
    rev_map *x = (rev_map *) obj;
    int i, j;
    (*x)(5, &i, &j);
    std::cout << "i: " << i << ", j: " << j << std::endl;
}

int main (){
    int n = 3;
    rev_map x(n);
    foo((void*)&x);
}

Polimorfismo

Além da simplicidade, outra vantagem de passar objetos é fazer uso do polimorfismo, característico de orientação a objetos.

No meu código, eu escrevi uma classe encapsulando a API do Xpress, para facilitar a criação de modelos PLI, a qual chamei de Xpress. Ao criar um modelo novo, eu geralmente defino uma nova classe herdando de Xpress. Considere Model1 e Model2 dois tais modelos.

struct Xpress {

};
struct Model1 : Xpress {

};
struct Model2 : Xpress {

};

Cada modelo tem seu conjunto de variáveis, mas eu gostaria que a callback imprimisse o nome das variáveis de maneira transparente. A minha solução foi definir rev_map como classes internas a esses modelos, inclusive na classe Xpress, pois é essa classe que vai garantir a transparência.

struct Xpress {
    struct rev_map {
    };
};
struct Model1 : Xpress {    
    struct rev_map : Xpress::rev_map {
    };
};
struct Model2 : Xpress {
    struct rev_map : Xpress::rev_map {
    };
};

A vantagem de usar classes internas é que elas podem ter o mesmo nome. A desambiguação é sempre feita de acordo com o escopo. Para selecionar rev_map de uma classe específica, basta qualificá-lo como por exemplo Xpress::rev_map.

E mais, como iremos chamar o operator (), recebendo col, *i, *j, para todo objeto rev_map, devemos forçar que todas as classes derivadas de Xpress::rev_map implementem-no. Podemos fazer com que rev_map em Xpress se comporte como uma interface, tornando-a puramente virtual:

struct Xpress {
    struct rev_map {
        virtual void operator() 
            (int col, int *i, int *j) = 0; 
    };
};

O restante do modelo fica:

struct Model1 : Xpress {
    
    struct rev_map : Xpress::rev_map {
        void operator() (int col, int *i, int *j) {
            printf("model1\n");
        } 
    };
};
struct Model2 : Xpress {
    struct rev_map : Xpress::rev_map {
        void operator() (int col, int *i, int *j) {
            printf("model2\n");
        } 
    };
};

void foo(void *obj){

    Xpress::rev_map *x = (Xpress::rev_map *) obj;
    int i, j;
    (*x)(5, &i, &j);
}

Para verificar o polimorfismo em ação:

int main (){

    Model1::rev_map a;
    foo((void*)&a); // imprime model1
    Model2::rev_map b;
    foo((void*)&b); // imprime model2
}

Veja que a callback (foo) vai invocar corretamente as classes internas de Model1 e Model2, mesmo que em sua implementação seja usada a classe interna de Xpress.


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!