Ponto V!

Home WebGL O primeiro triângulo
Vinícius Godoy de Mendonça
O primeiro triânguloImprimir
Escrito por Vinícius Godoy de Mendonça

No pipeline programável da WebGL desenhar um simples triângulo não é uma tarefa trivial. Para que isso seja possível, precisamos fornecer posições de vértices, escrever, compilar e linkar nossos shaders, além de configurar uma série de propriedades. Neste artigo veremos como usar o canvas descrito no artigo anterior para desenhar nosso primeiro triângulo. Preparado para começar?

Primitivas, triângulos e malhas

Todo desenho na WebGL é realizado através da especificação de um conjunto de vértices associado a uma primitiva gráfica. As primitivas gráficas indicam como os vértices devem ser ligados para desenhar triângulos, linhas, ou outras formas que tivermos interesse. O conjunto básico de primitivas é:

Primitivas gráficas: points, lines, line strip, line loop, triangles, triangle strip, triangle fan, quads, quad strip e polygon

Embora existam várias primitivas, as aplicações modernas trabalham basicamente com a primitiva GL_TRIANGLES. Toda a cena é desenhada através de pequenos triângulos, que combinadas formam todos os tipos de polígonos. Essa primitiva gráfica é mais conveniente por vários motivos:

  • Cada triângulo ocupa apenas um plano no espaço
  • Triângulos são polígonos convexos, ou seja, não é necessário se preocupar com buracos ou descontinuidades no momento de seu desenho
  • A placa gráfica nada mais é do que um hardware especializado em desenhar triângulos. E ela faz isso de maneira muito rápida.

    Aliás, em toda aplicação 3D, devemos lembrar que estamos sempre lidando com 2 hardwares diferentes:

  • A CPU: Que estamos acostumados, com a memória RAM, o sistema operacional e o navegador com o JavaScript
  • E a GPU: Que é a placa de vídeo. Ela que também possui uma área de memória própria.
  • Chamamos de malha poligonal (mesh) um conjunto de vértices associados entre si. Além da posição, os vértices podem conter diversos outros atributos como, por exemplo, cores.

    As malhas são carregadas do PC em arrays comuns no JavaScript, mas devem ser enviadas para a GPU através do que a WebGL chama de Array Buffers. Esses buffers representam a memória da placa de vídeo. Esteja atento que a comunicação entre a GPU e a CPU é um dos principais gargalos de performance em aplicações gráficas.

    Criando o triângulo

    Para configurar nossa malha na WebGL precisaremos executar 3 passos simples:

  • Criar um array, contendo a posição de todos os vértices que iremos desenhar. A ordem em que fornecemos esses vértices define o lado para o qual o triângulo estará virado: para frente se os vértices estiverem especificados em sentido anti-horário, ou para trás, se for em sentido horário;
  • Criar um Array Buffer e enviar para a placa de vídeo os dados do array criado.
  • Anotar detalhes sobre o array criado para futuro uso. Esse passo também é opcional, mas facilita bastante o uso futuro do array buffer.
  • Criaremos uma variável global chamada vertices que conterá nosso ArrayBuffer finalizado. A função initBuffers() é responsável pela realização desses passos:

    function initBuffers() {
        var positions = [
            0.0, 0.8, 0.0,
           -0.8, -0.8, 0.0,
            0.8, -0.8, 0.0
        ];
        
        //Solicita um buffer na placa de video
        vertices = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertices);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
            
        //Opcional: Gravar informações sobre os vértices    
        vertices.itemSize = 3;
        vertices.numItems = 3;    
    }

    Como a WebGL utiliza um conceito de máquina de estados, observe que tivemos que dar o comando gl.bindBuffer para indicar a API que iríamos trabalhar com o buffer vertices criado na linha superior. A partir desse momento, todos os comandos relativos a buffers serão realizados no buffer vertices. É o caso do comando gl.bufferData, que preenche a memória da placa com o buffer passado por parâmetro. Os argumentos desse comando:

  • O tipo de buffer: nesse caso, um ARRAY_BUFFER;
  • Os dados: note que o formato de dados suportado pela WebGL é o Float32. Como o JavaScript tem tipagem dinâmica, usamos o construtor da classe Float32Array para garantir que o array fornecido contivesse só dados desse tipo.
  • O tipo de uso que faremos do buffer: Ao usarmos STATIC_DRAW nesse parâmetro indicamos a WebGL que, uma vez que o buffer estiver criado, seus dados não irão mais mudar de valor. Ao saber disso, a WebGL tem mais liberdade de manter os dados na placa de vídeo. Além desse valor, poderíamos ter fornecido DYNAMIC_DRAW. Apesar da flexibilidade, modificar os dados trás o custo de mais comunicações entre a placa de vídeo e a CPU. Mais detalhes e funções podem ser verificados no guia de referência rápido oficial.
  • Ao final, anotamos no objeto vertices dois atributos interessantes. Primeiro, como estamos lidando com posições, que tem três dimensões (x, y e z), indicamos que o tamanho de cada item dentro do array é 3. Depois, como fornecemos 3 vértices, indicamos que o número de itens (vértices) no array também são 3.

    Observe também que a variável positions é uma variável local e, portanto, será destruída ao final da função. Isso não é um problema. Uma vez que os vértices já foram copiados para o Array Buffer, é papel da WebGL conservar essa informação na placa gráfica ou na CPU.

    Shaders

    Antes de iniciar qualquer desenho, precisamos explicar para a placa de vídeo como ela deve posicionar vértices e como ela deve pintar os pixels. Essa tarefa é realizada por dois programas que devem ser fornecidos por nós e rodam diretamente na placa: o vertex shader e o fragment shader (pixel shader). Os dois programas são criados na linguagem GLSL.

    Os primeiros shaders

    A cada vértice desenhado o Vertex Shader é acionado para realizar cálculos específicos. Dentre eles, o cálculo mais importante é o da posição do vértice. Todos os vértices da cena, ao final do desenho, devem considerar que a menor e a maior coordenada possível tanto no eixo x, quanto no y, é –1 e 1. Logicamente, seria muito difícil criar um game inteiro respeitando essa regra, por isso, é papel do vertex shader realizar conversões através de matrizes de transformação para ajustar as coordenadas em 3D mais próximas do nosso dia-a-dia nesse sistema.

    Como nosso triângulo foi fornecido considerando as coordenadas finais no intervalo máximo de –1 até 1, vamos deixar esse assunto para o próximo artigo, e especificar um vertex shader muitíssimo simples, que simplesmente repassa as coordenadas do vértice diretamente para a placa de vídeo:

    attribute vec3 aVertexPosition;
    
    void main(void)
    {
        gl_Position = vec4(aVertexPosition, 1.0);
    }

    Logo no início do shader, declaramos uma variável chamada aVertexPosition. Essa variável possui o tipo vec3 indicando que se trata de um vetor matemático contendo 3 dimensões (x, y e z). Além disso, também declaramos que essa variável é uma variável do tipo attribute, ou seja, contém atributos do vértice. Ao declarar isso, a WebGL entende que ele deve colocar nessa variável o valor correspondente ao vértice sendo desenhado no momento, assim, não precisaremos usar índices para nos referenciar ao array de vértices dentro do shader.

    A posição final do vértice deve ser gravada na variável gl_Position. Essa variável existe automaticamente dentro do shader, e deve conter um vetor de 4 coordenadas (x, y, z e w). Tratam-se das coordenadas transformadas do vértice no sistema coordenadas homogêneo. Por hora, basta sabermos

    Da mesma forma, durante a fase de rasterização, a cada pixel desenhado o Fragment Shader será consultado para determinar a cor do pixel. Iniciaremos definindo um shader que simplesmente retorna a cor amarela:

    void main(void)
    {
        gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
    }

    No fragment shader as cores são sempre representadas no formato R, G, B e A com seus intervalos variando de 0 até 1, e não de 0 até 255. Assim, o tom 128 é representada pelo número 0.5. No futuro, veremos que essa é uma forma muito conveniente de representar cores em computação gráfica.

    Compilando e linkando os shaders

    Para compilar um shader, devemos seguir os passos abaixo:

    • Criar o shader, especificando seu tipo
    • Vincular a WebGL ao shader específico
    • Chamar o comando de compilação
    • Testar se não houve erros

    Para realizar esses passos, vamos criar uma função chamada loadShader. Ela recebe dois parâmetros, o tipo e o nome do shader a ser compilado:

    function loadShader(type, code) {
        //Cria o id para o shader de acordo com o tipo
        var shader = gl.createShader(type);
        
        //Compila o shader
        gl.shaderSource(shader, code);
        gl.compileShader(shader);
        
        //Testa se houve erro
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            alert(gl.getShaderInfoLog(shader));
            return null;
        }
        
        return shader;
    }

    Passar um shader para essa função é suficiente para descobrirmos se cometemos erros de código, porém, não é o suficiente para executar esses shaders. Para que seja, ainda precisamos associar um shader ao outro shaders, numa estrutura conhecida como shader program. Esse processo é chamado de linking. Vamos criar uma função chamada linkProgram que associa dois shaders juntos:

    function linkProgram(vs, fs) {
        var shaderProgram = gl.createProgram();
        gl.attachShader(shaderProgram, vs);
        gl.attachShader(shaderProgram, fs);
        gl.linkProgram(shaderProgram);
        
        if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
            alert("Could not link shaders!");        
        }
        return shaderProgram;
    }

    O últiimo passo necessário é realizar a chamada a essas funções com nossos shaders específicos:

    function initShaders() {
        var vertexShader = "\
            attribute vec3 aVertexPosition;\n\
            void main(void)\n\
            {\n\
                 gl_Position = vec4(aVertexPosition, 1.0);\n\
            }";
    
        var fragmentShader = "\
            void main(void)\n\
            {\n\
                gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);\n\
            }";
    
        var vs = loadShader(gl.VERTEX_SHADER, vertexShader);
        var fs = loadShader(gl.FRAGMENT_SHADER, fragmentShader);
        
        shaderProgram = linkProgram(vs, fs);
    }

    Não se preocupe com o fato dos shaders estarem diretamente declarados em strings por enquanto. Para frente, veremos como carregá-los do servidor.

    A função initShaders() deve ser chamada logo depois da função initBuffers(), dentro do método startWebGL().

    Desenhando

    Antes de utilizarmos o shader para desenhar, precisamos ter uma forma de vincular a variável do vertex shader aVertexPosition com o ArrayBuffer vertices criado em nosso JavaScript.

    Fazemos isso em quatro etapas, sendo que, nas duas primeiras:

  • Solicitamos ao shader um ponteiro para esse atributo. É conveniente fazer isso na função initShaders, e gravar esse ponteiro como uma propriedade adicional de nosso program;
  • Dizemos a webgl que esse atributo está habilitado (deve ser considerado para o desenho);

    gl.useProgram(shaderProgram);
    shaderProgram.aVertexPosition = gl.getAttribLocation(shaderProgram, "aVertexPosition");        
    gl.enableVertexAttribArray(shaderProgram.aVertexPosition);    

    Note que criamos no objeto shaderProgram a propriedade aVertexPosition contendo o ponteiro para a variável de mesmo nome dentro do shader. Esse ponteiro foi obtido com a função gl.getAttribLocation. Obviamente, o atributo poderia ter um nome completamente diferente, mas é conviente manter os nomes iguais para não nos perdermos depois. Em seguida, usamos a função gl.enableVertexAttribArray para informar a WebGL que esse atributo deve ser considerado durante a pintura.

    Finalmente, realizamos o desenho. Para isso, precisamos:

  • Preencher as variáveis do shader com os valores que efetivamente serão usados;
  • Chamar a função de desenho indicando que primitiva será desenhada.
  • function drawScene() {
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);        
            
        //Copia dados do buffer
        gl.bindBuffer(gl.ARRAY_BUFFER, vertices);
        gl.vertexAttribPointer(shaderProgram.aVertexPosition, vertices.itemSize, gl.FLOAT, false, 0, 0);
        
        //Comanda o desenho
        gl.drawArrays(gl.TRIANGLES, 0, vertices.numItems);
    }

    O comando gl.vertexAttribPointer é o responsável por associar o ArrayBuffer previamente vinculado com gl.bindBuffer a variável do shader indicada pelo primeiro parâmetro. Além disso, é necessário dizer qual é o tamanho dos elementos dentro desse array (no caso de posição 3 valores – x, y e z), o tipo de dado desse elemento (floats). O próximo atributo indica se os valores de float devem ser normalizados antes de serem usados e, no caso da webgl, esse valor deve ser sempre false, já que implementação para navegadores não dá suporte a normalização. Finalmente, os dois últimos atributos indicam quantos bytes devem ser pulados entre um atributo e outro e qual é o índice do primeiro byte do atributo no array.

    No caso de nosso desenho, ambos zero. Os dois últimos valores podem ser usados para guardar diversos atributos distintos num mesmo array. Isso é mais útil em C ou C++, onde uma struct ou objeto pode ser diretamente transformado em bytes na memória. Em javascript, usaremos arrays separados para cada atributo.

    Por fim, o comando gl.drawArrays desenha o triângulo na tela. Seu primeiro parâmetro é a primitiva gráfica a ser usada. O segundo qual é o primeiro índice a ser desenhado. E o terceiro, quantos índices no total serão desenhados.

    Triângulo amarelo

    Uma observação importante. O desenho não é imediatamente desenhado após o comando gl.drawArrays. Quem realmente fará o desenho é a placa de vídeo. É inútil, portanto, tentar medir o tempo que o desenho levou para acontecer colocamos timers antes e depois da chamada da função de desenho.

    Download

    Muitos detalhes? O código não rodou? Baixe os fontes complexos aqui:

    Códigos fonte

    Concluindo

    Neste artigo, criamos nosso primeiro triângulo. A configuração inicial é bastante árdua, e exige um bocado de detalhes. No próximo artigo, iremos melhorar um pouco nosso Vertex Shader para permitir desenhar o triângulo em diferentes posições, usando para isso matrizes de transformação. Se você quiser já ir aquecendo seus motores, confira o artigo sobre vetores e nossos dois artigos sobre matrizes de transformação.


    Comentários (0)
    Escrever um comentário
    Your Contact Details:
    Gravatar enabled
    Comentário:
    [b] [i] [u] [url] [quote] [code] [img]   
    :angry::0:confused::cheer:B):evil::silly::dry::lol::kiss::D:pinch::(:shock:
    :X:side::):P:unsure::woohoo::huh::whistle:;):S:!::?::idea::arrow:
    Security
    Por favor coloque o código anti-spam que você lê na imagem.
    LAST_UPDATED2  

    Busca

    Linguagens

    Twitter