Tutorial 2

Witaj w drugiej części. Tym razem będziemy nasze figury kolorować. Zanim jednak przejdziemy do nadawania kolorów naszej scenie podsumujmy co zrobiliśmy ostatnio idąc z góry na dół: W tym przykładzie będziemy zmieniali kod tylko w shaderach, initBuffers oraz drawScene. Aby wiedzieć jak te zmiany zachodzą konieczne jest zrozumienie jak są renderowane obiekty w WebGL'u. Przedstawia to poniższy rysunek. Diagram przedstawia w uproszczony sposób jak dane przekazywane do JavaScriptu zmieniaja się w piksele w WebGL'u.

Na najwyższym poziomie wygląda to tak: za każym razem gdy wywołujesz funkcję jak na przykład drawArrays, WebGL przetwarza dane które zostały mu wcześniej przekazane w postaci atrybutów oraz uniform variables(których używaliśmy przy macierzach projekci i modelu) i przekazuje to do vertex shadera.

Robi to wywołując shadera wierzchołków po razie dla każdego z nich, za każdym razem z atrybutami odpowiednimi dla każdego wierzchołka. Ponadto przekazuje uniform varibales. Jak ich nazwa wskazuje nie zmieniają się od wywołania do wywołania. Shader zastosowuje do nich obie znane nam macierze i wyniki umieszcza w zmiennych zwanych varying variables. Są one interfejsem pomiędzy vertex a fragment shaderem. Takich zmiennych może być wiele jednak jedną jedyną jaka jest obowiązkowa to glPosition, która zawiera koordynaty wierzchołka nad którym shader właśnie przestał pracować.

Następnie WebGL dokounje rasteryzacji, czyli zmienia obraz 3D w 2D. Tak naprawdę to modyfikuje varying variables i następnie wywołuje dla każdego piksela z obrazu fragment shadera. Decyduje on które piksele mają zostać wypełnione a które nie i jakim kolorem.

Ostatecznie dane są jeszcze trochę przetwarzane przez WebGL'a i przekazywane do frame buffer'a, któy już przechowuje to co jest wyświetlane.

Celem tej lekcji jest pokazanie jak z JavaScript przekazać informacje o kolorze do frament shadera nie mając do niego bezpośredniego dostępu.

Sposobem na dokonanie tego jest wykorzystanie faktu, że vertex shader może przekazać wiecej varying variables. Nie tylko glPosition. Czyli przekazujemy kolor do vertex shadera, który przekaże te informacje prosto do fragment shadera.

Spójrzmy wreszcie na kod. Na dobry początek przyjrzyjmy się vertex shaderowi:

  attribute vec3 aVertexPosition;
  attribute vec4 aVertexColor;

  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;

  varying vec4 vColor;

  void main(void) {
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
    vColor = aVertexColor;
  }
    

Łatwo zauważyć, że doszły nam dwa atrybuty - rózne dla każdego z wierzchołków oraz zmienna wyjściowa vColor. Pozycja jest dokładnie tak samo liczona jak wcześniej. Kolor natomiast jest przekazywany z wejścia prosto do wyjścia.

Fragment shader teraz nie definiuje sztywno koloru jak wcześniej tylko pobiera z wejścia zmienną:

        precision mediump float;

        varying vec4 vColor;

        void main(void) {
            gl_FragColor = vColor;
        }
    

Kolejna mała zmiana zachodzi w inicjalizacji shaderów. Teraz pobieramy referencji do dwóch atrybutów a nie jednego. Dodatkowe linie są zaznaczone

    var shaderProgram;
    function initShaders() {
        var fragmentShader = getShader(gl, "shader-fs");
        var vertexShader = getShader(gl, "shader-vs");

        shaderProgram = gl.createProgram();
        gl.attachShader(shaderProgram, vertexShader);
        gl.attachShader(shaderProgram, fragmentShader);
        gl.linkProgram(shaderProgram);

        if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
          alert("Could not initialise shaders");
        }

        gl.useProgram(shaderProgram);

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

        shaderProgram.vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor");
        gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute);

        shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
        shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
    }
    

Przeanalizujmy teraz jakie zmiany miały miejsce w funkcji initBuffers. Zmiany zostały podświetlone.

        var triangleVertexPositionBuffer;
        var triangleVertexColorBuffer;
        var squareVertexPositionBuffer;
        var squareVertexColorBuffer;

        function initBuffers() {
        triangleVertexPositionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
        var vertices = [
             0.0,  1.0,  0.0,
            -1.0, -1.0,  0.0,
             1.0, -1.0,  0.0
        ];
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
        triangleVertexPositionBuffer.itemSize = 3;
        triangleVertexPositionBuffer.numItems = 3;

        triangleVertexColorBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
        var colors = [
            1.0, 0.0, 0.0, 1.0,
            0.0, 1.0, 0.0, 1.0,
            0.0, 0.0, 1.0, 1.0
        ];
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
        triangleVertexColorBuffer.itemSize = 4;
        triangleVertexColorBuffer.numItems = 3;


        squareVertexPositionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
        vertices = [
             1.0,  1.0,  0.0,
            -1.0,  1.0,  0.0,
             1.0, -1.0,  0.0,
            -1.0, -1.0,  0.0
        ];
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
        squareVertexPositionBuffer.itemSize = 3;
        squareVertexPositionBuffer.numItems = 4;

        squareVertexColorBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
        colors = [];
        for (var i=0; i < 4; i++) {
            colors = colors.concat([0.5, 0.5, 1.0, 1.0]);
        }
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
        squareVertexColorBuffer.itemSize = 4;
        squareVertexColorBuffer.numItems = 4;
    }
    

Na dobry początek definiujemy dwie globalne zmienne, które będą pełniły rolę buforów koloru. Następnie po stworzeniu bufora pozycji zabieramy się do wypełnienia bufora kolorów odpowiednimi informacjami. Robimy to w bardzo podobny sposób. Tutaj zamiast definiować współrzędne wierzchołka określamy jaki przy nim będzie kolor w kolejności Czerwony, Zielony, Niebieski, Aplha. Wartośc "1.0" jest maksymalną możliwą. Parametr Aplha mówi o przezroczystości. Analogicznie definiujemy kolory dla kwadratu.

Ostatnie zmiany zachodzą w funkcji drawScene. Jest to zrobienie użytku z nowych danych.

        function drawScene() {
        gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);

        mat4.identity(mvMatrix);

        mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);
        gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

        gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, triangleVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

        setMatrixUniforms();
        gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);

        mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);
        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

        setMatrixUniforms();
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
    }
    

To by było na tyle. Następnym razem będziemy ruszać naszymi kolorowymi figurami.

Wynik końcowy