WebGL ile ilgili temel bilgiler

Gregg Tavares
Gregg Tavares

WebGL, tarayıcınızda muhteşem gerçek zamanlı 3D grafikler görüntülemenizi sağlar ancak birçok kişinin bilmediği şey, WebGL'nin aslında bir 3D API değil, 2D API olduğudur. Bunu şöyle açıklayabiliriz.

WebGL yalnızca iki şeyi önemser. 2D ve renklerde kırpma alanı koordinatları. WebGL kullanan bir programcı olarak göreviniz, WebGL'ye bu iki şeyi sağlamaktır. Bunun için 2 "gölgelendirici" sağlarsınız. Kırpma alanı koordinatlarını sağlayan bir tepe gölgelendirici ve rengi sağlayan bir parça gölgelendirici. Kırpma alanı koordinatları, tuvalinizin boyutu ne olursa olsun her zaman -1 ile +1 arasında değişir. WebGL'yi en basit haliyle gösteren basit bir WebGL örneğini aşağıda bulabilirsiniz.

// Get A WebGL context
var canvas = document.getElementById("canvas");
var gl = canvas.getContext("experimental-webgl");

// setup a GLSL program
var vertexShader = createShaderFromScriptElement(gl, "2d-vertex-shader");
var fragmentShader = createShaderFromScriptElement(gl, "2d-fragment-shader");
var program = createProgram(gl, [vertexShader, fragmentShader]);
gl.useProgram(program);

// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");

// Create a buffer and put a single clipspace rectangle in
// it (2 triangles)
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([
        -1.0, -1.0,
         1.0, -1.0,
        -1.0,  1.0,
        -1.0,  1.0,
         1.0, -1.0,
         1.0,  1.0]),
    gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

// draw
gl.drawArrays(gl.TRIANGLES, 0, 6);

İşte 2 gölgelendirici

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

void main() {
  gl_Position = vec4(a_position, 0, 1);
}
</script>

<script id="2d-fragment-shader" type="x-shader/x-fragment">
void main() {
  gl_FragColor = vec4(0,1,0,1);  // green
}
</script>

Kırpma alanı koordinatları, tuvalin boyutundan bağımsız olarak her zaman -1 ile +1 arasında olur. Yukarıdaki örnekte, konum verilerimizi doğrudan iletmekten başka bir şey yapmadığımızı görebilirsiniz. Konum verileri zaten kırpma uzayında olduğundan yapılması gereken bir şey yoktur. 3D istiyorsanız 3D'den 2D'ye dönüştüren gölgelendiricileri sağlamak size kalmıştır çünkü WebGL, 2D API'dir. 2D şeyler için muhtemelen kırpma alanı yerine piksellerle çalışmayı tercih edersiniz. Bu nedenle, gölgelendiriciyi değiştirerek piksellerle dikdörtgenler sağlayıp bizim için kırpma alanına dönüştürmesini sağlayalım. Yeni köşe gölgelendirici:

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform vec2 u_resolution;

void main() {
   // convert the rectangle from pixels to 0.0 to 1.0
   vec2 zeroToOne = a_position / u_resolution;

   // convert from 0->1 to 0->2
   vec2 zeroToTwo = zeroToOne * 2.0;

   // convert from 0->2 to -1->+1 (clipspace)
   vec2 clipSpace = zeroToTwo - 1.0;

   gl_Position = vec4(clipSpace, 0, 1);
}
</script>

Artık verilerimizi kırpma alanından piksele dönüştürebiliriz.

// set the resolution
var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);

// setup a rectangle from 10,20 to 80,30 in pixels
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
    10, 20,
    80, 20,
    10, 30,
    10, 30,
    80, 20,
    80, 30]), gl.STATIC_DRAW);

Dikdörtgenin bu alanın alt kısmına yakın olduğunu fark edebilirsiniz. WebGL, sol alt köşeyi 0,0 olarak kabul eder. 2D grafik API'leri için kullanılan daha geleneksel sol üst köşeye getirmek için y koordinatını ters çeviririz.

gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

Dikdörtgeni tanımlayan kodu bir işlev haline getirelim. Böylece bu işlevi farklı boyutlardaki dikdörtgenler için çağırabiliriz. Bu arada rengi de ayarlanabilir hale getireceğiz. Öncelikle parça gölgelendiricinin tek tip bir renk girişi almasını sağlayalım.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

uniform vec4 u_color;

void main() {
   gl_FragColor = u_color;
}
</script>

Rastgele yerlerde ve rastgele renklerde 50 dikdörtgen çizen yeni kod ise burada.

...

  var colorLocation = gl.getUniformLocation(program, "u_color");
  ...
  // Create a buffer
  var buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.enableVertexAttribArray(positionLocation);
  gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

  // draw 50 random rectangles in random colors
  for (var ii = 0; ii < 50; ++ii) {
    // Setup a random rectangle
    setRectangle(
        gl, randomInt(300), randomInt(300), randomInt(300), randomInt(300));

    // Set a random color.
    gl.uniform4f(colorLocation, Math.random(), Math.random(), Math.random(), 1);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  }
}

// Returns a random integer from 0 to range - 1.
function randomInt(range) {
  return Math.floor(Math.random() * range);
}

// Fills the buffer with the values that define a rectangle.
function setRectangle(gl, x, y, width, height) {
  var x1 = x;
  var x2 = x + width;
  var y1 = y;
  var y2 = y + height;
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
     x1, y1,
     x2, y1,
     x1, y2,
     x1, y2,
     x2, y1,
     x2, y2]), gl.STATIC_DRAW);
}

WebGL'nin aslında oldukça basit bir API olduğunu görebilirsiniz. 3D grafikler oluşturmak daha karmaşık olabilir ancak bu karmaşıklık, programcı olarak sizin daha karmaşık gölgelendiriciler eklemenizden kaynaklanır. WebGL API'nin kendisi 2D'dir ve oldukça basittir.

type="x-shader/x-vertex" ve type="x-shader/x-fragment" ne anlama gelir?

<script> etiketleri varsayılan olarak JavaScript içerir. Tür belirtmeyebilir veya type="javascript" ya da type="text/javascript" kullanabilirsiniz. Tarayıcı, içeriği JavaScript olarak yorumlar. Başka bir şey girerseniz tarayıcı, komut dosyası etiketinin içeriğini yoksayar.

Bu özelliği, gölgelendiricileri komut dosyası etiketlerinde depolamak için kullanabiliriz. Daha da iyisi, kendi türümüzü oluşturabilir ve JavaScript'imizde gölgelendiriciyi köşe gölgelendirici olarak mı yoksa parça gölgelendirici olarak mı derleyeceğimize karar vermek için bu türü arayabiliriz.

Bu durumda createShaderFromScriptElement işlevi, belirtilen id değerine sahip bir komut dosyası arar ve ardından hangi gölgelendirici türünün oluşturulacağına karar vermek için type değerine bakar.

WebGL görüntü işleme

WebGL'de görüntü işleme kolaydır. Ne kadar kolay? Aşağıdaki bölümü okuyun.

WebGL'de resim çizmek için dokuları kullanmamız gerekir. WebGL, oluşturma sırasında piksel yerine kırpma alanı koordinatları beklediği gibi, doku okurken de doku koordinatları bekler. Doku koordinatları, dokunun boyutları ne olursa olsun 0,0 ile 1,0 arasındadır. Yalnızca tek bir dikdörtgen (yani 2 üçgen) çizdiğimiz için WebGL'ye dikdörtgendeki her noktanın dokudaki hangi yere karşılık geldiğini söylememiz gerekir. Bu bilgiyi, "varying" adı verilen özel bir değişken türü kullanarak köşe gölgelendiriciden parça gölgelendiriciye aktaracağız. Değişken olarak adlandırılmasının nedeni, değişmesidir. WebGL, her pikseli parça gölgelendiriciyi kullanarak çizerken köşe gölgelendiricide sağladığımız değerleri enterpolasyonla hesaplar. Önceki bölümün sonundaki köşe gölgelendiriciyi kullanarak doku koordinatlarını iletmek için bir özellik eklememiz ve ardından bunları parça gölgelendiriciye iletmemiz gerekir.

attribute vec2 a_texCoord;
...
varying vec2 v_texCoord;

void main() {
   ...
   // pass the texCoord to the fragment shader
   // The GPU will interpolate this value between points
   v_texCoord = a_texCoord;
}

Ardından, dokudaki renkleri aramak için bir parça gölgelendirici sağlarız.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// our texture
uniform sampler2D u_image;

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   // Look up a color from the texture.
   gl_FragColor = texture2D(u_image, v_texCoord);
}
</script>

Son olarak bir resim yüklememiz, doku oluşturmamız ve resmi dokuya kopyalamamız gerekir. Tarayıcıda olduğumuz için resimler eşzamansız olarak yüklenir. Bu nedenle, dokunun yüklenmesini beklemek için kodumuzu biraz yeniden düzenlememiz gerekir. Yüklendikten sonra çizeceğiz.

function main() {
  var image = new Image();
  image.src = "http://someimage/on/our/server";  // MUST BE SAME DOMAIN!!!
  image.onload = function() {
    render(image);
  }
}

function render(image) {
  ...
  // all the code we had before.
  ...
  // look up where the texture coordinates need to go.
  var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");

  // provide texture coordinates for the rectangle.
  var texCoordBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
      0.0,  0.0,
      1.0,  0.0,
      0.0,  1.0,
      0.0,  1.0,
      1.0,  0.0,
      1.0,  1.0]), gl.STATIC_DRAW);
  gl.enableVertexAttribArray(texCoordLocation);
  gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);

  // Create a texture.
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Set the parameters so we can render any size image.
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

  // Upload the image into the texture.
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  ...
}

Pek heyecan verici değil, bu yüzden bu resmi değiştirelim. Kırmızı ve mavinin yerini değiştirmeye ne dersiniz?

...
gl_FragColor = texture2D(u_image, v_texCoord).bgra;
...

Diğer piksellere de bakan bir görüntü işleme yapmak istersek ne olur? WebGL, dokulara 0,0 ile 1,0 arasındaki doku koordinatlarında referans verdiğinden, 1 piksel için ne kadar hareket etmemiz gerektiğini basit bir matematik işlemiyle (onePixel = 1.0 / textureSize) hesaplayabiliriz. Aşağıda, dokudaki her pikselin sol ve sağ piksellerinin ortalamasını alan bir parça gölgelendirici verilmiştir.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   // compute 1 pixel in texture coordinates.
   vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;

   // average the left, middle, and right pixels.
   gl_FragColor = (
       texture2D(u_image, v_texCoord) +
       texture2D(u_image, v_texCoord + vec2(onePixel.x, 0.0)) +
       texture2D(u_image, v_texCoord + vec2(-onePixel.x, 0.0))) / 3.0;
}
</script>

Ardından, JavaScript'ten dokunun boyutunu iletmemiz gerekir.

...
var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize");
...
// set the size of the image
gl.uniform2f(textureSizeLocation, image.width, image.height);
...

Diğer piksellere nasıl referans vereceğimizi öğrendiğimize göre, bir dizi yaygın görüntü işleme işlemini gerçekleştirmek için evrişim çekirdeği kullanalım. Bu örnekte 3x3'lük bir çekirdek kullanacağız. Evrişim çekirdeği, matristeki her girişin, oluşturduğumuz pikselin etrafındaki 8 pikselin ne kadar çarpılacağını temsil ettiği 3x3'lük bir matristir. Ardından, sonucu çekirdeğin ağırlığına (çekirdekteki tüm değerlerin toplamı) veya 1,0'a (hangisi daha büyükse) böleriz. Bu konuyla ilgili oldukça iyi bir makaleyi burada bulabilirsiniz. Bu makalede ise C++ ile manuel olarak yazmanız durumunda kullanacağınız kod gösteriliyor. Biz bu işlemi gölgelendiricide yapacağız. Yeni parça gölgelendiriciyi aşağıda görebilirsiniz.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;
uniform float u_kernel[9];

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
   vec4 colorSum =
     texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1, -1)) * u_kernel[2] +
     texture2D(u_image, v_texCoord + onePixel * vec2(-1,  0)) * u_kernel[3] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0,  0)) * u_kernel[4] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1,  0)) * u_kernel[5] +
     texture2D(u_image, v_texCoord + onePixel * vec2(-1,  1)) * u_kernel[6] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0,  1)) * u_kernel[7] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1,  1)) * u_kernel[8] ;
   float kernelWeight =
     u_kernel[0] +
     u_kernel[1] +
     u_kernel[2] +
     u_kernel[3] +
     u_kernel[4] +
     u_kernel[5] +
     u_kernel[6] +
     u_kernel[7] +
     u_kernel[8] ;

   if (kernelWeight <= 0.0) {
     kernelWeight = 1.0;
   }

   // Divide the sum by the weight but just use rgb
   // we'll set alpha to 1.0
   gl_FragColor = vec4((colorSum / kernelWeight).rgb, 1.0);
}
</script>

JavaScript'te bir evrişim çekirdeği sağlamamız gerekir.

...
var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
...
var edgeDetectKernel = [
    -1, -1, -1,
    -1,  8, -1,
    -1, -1, -1
];
gl.uniform1fv(kernelLocation, edgeDetectKernel);
...

Umarım bu, WebGL'de görüntü işlemenin oldukça basit olduğuna sizi ikna etmiştir. Şimdi de resme birden fazla efektin nasıl uygulanacağını anlatacağım.

GLSL'de değişkenlerin önündeki a, u ve v_ önekleri ne anlama geliyor?

Bu sadece bir adlandırma kuralıdır. a_, tamponlar tarafından sağlanan veriler olan özellikler için kullanılır. Gölgeleyicilere giriş olan uniform'lar için u_, bir köşe gölgeleyiciden parça gölgeleyiciye aktarılan ve çizilen her piksel için köşeler arasında enterpolasyon uygulanan (veya değiştirilen) değişkenler için v_.

Birden fazla efekt uygulama

Görüntü işleme ile ilgili bir sonraki en belirgin soru, birden fazla efektin nasıl uygulanacağıdır.

Gölgelendiricileri anında oluşturmayı deneyebilirsiniz. Kullanıcının kullanmak istediği efektleri seçmesine olanak tanıyan bir kullanıcı arayüzü sağlayın ve ardından tüm efektleri uygulayan bir gölgelendirici oluşturun. Bu her zaman mümkün olmayabilir ancak bu teknik genellikle gerçek zamanlı grafikler için efekt oluşturmak amacıyla kullanılır. Daha esnek bir yöntem ise 2 doku daha kullanmak ve her dokuyu sırayla oluşturmak, ileri geri gidip gelmek ve her seferinde bir sonraki efekti uygulamaktır.

Original Image -> [Blur]        -> Texture 1
Texture 1      -> [Sharpen]     -> Texture 2
Texture 2      -> [Edge Detect] -> Texture 1
Texture 1      -> [Blur]        -> Texture 2
Texture 2      -> [Normal]      -> Canvas

Bunun için kare arabellekleri oluşturmamız gerekir. WebGL ve OpenGL'de kare arabelleği aslında kötü bir addır. WebGL/OpenGL Framebuffer, aslında yalnızca bir durum koleksiyonudur ve herhangi bir türde arabellek değildir. Ancak bir kare arabelleğine doku ekleyerek bu dokuya görüntü oluşturabiliriz. Öncelikle eski doku oluşturma kodunu bir işleve dönüştürelim.

function createAndSetupTexture(gl) {
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Set up texture so we can render any size image and so we are
  // working with pixels.
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

  return texture;
}

// Create a texture and put the image in it.
var originalImageTexture = createAndSetupTexture(gl);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

Şimdi bu işlevi kullanarak 2 doku daha oluşturup bunları 2 kare arabelleğine ekleyelim.

// create 2 textures and attach them to framebuffers.
var textures = [];
var framebuffers = [];
for (var ii = 0; ii < 2; ++ii) {
  var texture = createAndSetupTexture(gl);
  textures.push(texture);

  // make the texture the same size as the image
  gl.texImage2D(
      gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0,
      gl.RGBA, gl.UNSIGNED_BYTE, null);

  // Create a framebuffer
  var fbo = gl.createFramebuffer();
  framebuffers.push(fbo);
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  // Attach a texture to it.
  gl.framebufferTexture2D(
      gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
}

Şimdi bir çekirdek grubu ve ardından uygulanacak bir liste oluşturalım.

// Define several convolution kernels
var kernels = {
  normal: [
    0, 0, 0,
    0, 1, 0,
    0, 0, 0
  ],
  gaussianBlur: [
    0.045, 0.122, 0.045,
    0.122, 0.332, 0.122,
    0.045, 0.122, 0.045
  ],
  unsharpen: [
    -1, -1, -1,
    -1,  9, -1,
    -1, -1, -1
  ],
  emboss: [
     -2, -1,  0,
     -1,  1,  1,
      0,  1,  2
  ]
};

// List of effects to apply.
var effectsToApply = [
  "gaussianBlur",
  "emboss",
  "gaussianBlur",
  "unsharpen"
];

Son olarak, her birini uygulayalım ve hangi dokuyu oluşturduğumuzu da değiştirelim.

// start with the original image
gl.bindTexture(gl.TEXTURE_2D, originalImageTexture);

// don't y flip images while drawing to the textures
gl.uniform1f(flipYLocation, 1);

// loop through each effect we want to apply.
for (var ii = 0; ii < effectsToApply.length; ++ii) {
  // Setup to draw into one of the framebuffers.
  setFramebuffer(framebuffers[ii % 2], image.width, image.height);

  drawWithKernel(effectsToApply[ii]);

  // for the next draw, use the texture we just rendered to.
  gl.bindTexture(gl.TEXTURE_2D, textures[ii % 2]);
}

// finally draw the result to the canvas.
gl.uniform1f(flipYLocation, -1);  // need to y flip for canvas
setFramebuffer(null, canvas.width, canvas.height);
drawWithKernel("normal");

function setFramebuffer(fbo, width, height) {
  // make this the framebuffer we are rendering to.
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  // Tell the shader the resolution of the framebuffer.
  gl.uniform2f(resolutionLocation, width, height);

  // Tell webgl the viewport setting needed for framebuffer.
  gl.viewport(0, 0, width, height);
}

function drawWithKernel(name) {
  // set the kernel
  gl.uniform1fv(kernelLocation, kernels[name]);

  // Draw the rectangle.
  gl.drawArrays(gl.TRIANGLES, 0, 6);
}

Üzerinden geçmem gereken bazı konular var.

gl.bindFramebuffer işlevini null ile çağırmak, WebGL'ye kare arabelleklerinizden biri yerine tuvalde oluşturma yapmak istediğinizi bildirir. WebGL'nin kırpma alanından tekrar piksele dönüştürmesi gerekir. Bu işlemi gl.viewport ayarlarına göre yapar. WebGL'yi başlattığımızda gl.viewport ayarları varsayılan olarak tuvalin boyutuna ayarlanır. Oluşturduğumuz çerçeve arabellekleri tuvalden farklı bir boyutta olduğundan görüntü alanını uygun şekilde ayarlamamız gerekir. Son olarak, WebGL ile ilgili temel bilgileri içeren örneklerde, WebGL tuvali 0,0 sol alt köşe olacak şekilde (2D için daha geleneksel olan sol üst köşe yerine) gösterdiğinden, oluşturma sırasında Y koordinatını ters çevirdik. Bir kare arabelleğine oluşturma işlemi yaparken buna gerek yoktur. Kare arabelleği hiçbir zaman gösterilmediğinden, hangi bölümün üst ve alt olduğu önemli değildir. Önemli olan, çerçeve arabelleğindeki 0,0 pikselinin hesaplamalarımızda 0,0'a karşılık gelmesidir. Bu sorunu çözmek için gölgelendiriciye bir giriş daha ekleyerek çevirme işleminin yapılıp yapılmayacağını ayarlamayı mümkün kıldım.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
...
uniform float u_flipY;
...

void main() {
   ...
   gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1);
   ...
}
</script>

Ardından, bu değeri, modelimizi oluştururken

...
var flipYLocation = gl.getUniformLocation(program, "u_flipY");
...
// don't flip
gl.uniform1f(flipYLocation, 1);
...
// flip
gl.uniform1f(flipYLocation, -1);

Birden fazla efekt oluşturabilen tek bir GLSL programı kullanarak bu örneği basit tuttum. Tam görüntü işleme yapmak istiyorsanız muhtemelen birçok GLSL programına ihtiyacınız olacaktır. Ton, doygunluk ve parlaklık ayarı için bir program. Diğeri ise parlaklık ve kontrast için. Biri ters çevirme, diğeri seviyeleri ayarlama vb. için. GLSL programları arasında geçiş yapmak ve ilgili programın parametrelerini güncellemek için kodu değiştirmeniz gerekir. Bu örneği yazmayı düşündüm ancak her birinin kendi parametre ihtiyaçları olan birden fazla GLSL programı, her şeyin büyük bir karmaşaya dönüşmesini önlemek için büyük bir yeniden düzenleme anlamına gelebileceğinden bu alıştırmayı okuyucuya bırakmak en iyisidir. Bu ve önceki örneklerin WebGL'i biraz daha anlaşılır hale getirdiğini ve 2D ile başlamanın WebGL'i anlamayı biraz daha kolaylaştırdığını umuyorum. Zaman bulursam 3D'nin nasıl yapılacağı hakkında birkaç makale daha yazmaya çalışacağım ve WebGL'nin arka planda neler yaptığı hakkında daha fazla bilgi vereceğim.

WebGL ve alfa

Bazı OpenGL geliştiricilerinin, WebGL'nin arka arabellekteki (ör. tuval) alfayı işleme şekliyle ilgili sorunlar yaşadığını fark ettim. Bu nedenle, WebGL ile OpenGL arasındaki alfayla ilgili bazı farkları incelemenin iyi olabileceğini düşündüm.

OpenGL ile WebGL arasındaki en büyük fark, OpenGL'nin hiçbir şeyle birleştirilmeyen veya işletim sisteminin pencere yöneticisi tarafından hiçbir şeyle birleştirilmeyen bir arka arabelleğe oluşturma yapmasıdır. Bu nedenle, alfa değerinizin ne olduğu önemli değildir. WebGL, tarayıcı tarafından web sayfasıyla birleştirilir ve varsayılan olarak, şeffaflık ve 2D tuval etiketleri içeren .png <img> etiketleriyle aynı şekilde önceden çarpılmış alfa kullanılır. WebGL'de bunu OpenGL'ye daha çok benzetmenin birkaç yolu vardır.

1. WebGL'ye, önceden çarpılmamış alfa ile birleştirilmesini istediğinizi bildirin.

gl = canvas.getContext("experimental-webgl", {premultipliedAlpha: false});

Varsayılan değer "true"dur. Elbette sonuç, tuvalin altında kalan arka plan rengi (tuvalin arka plan rengi, tuvalin kapsayıcı arka plan rengi, sayfanın arka plan rengi, tuvalin z-endeksi > 0 ise tuvalin arkasındaki öğeler vb.) ne olursa olsun sayfa üzerinde birleştirilir. Diğer bir deyişle, CSS'nin web sayfasının bu alanı için tanımladığı renk kullanılır. Alfa sorunlarınız olup olmadığını anlamanın en iyi yolu, tuvalin arka planını kırmızı gibi parlak bir renge ayarlamaktır. Neler olduğunu hemen görebilirsiniz.

<canvas style="background: red;"></canvas>

Ayrıca, alfa sorunlarını gizlemek için arka planı siyah olarak da ayarlayabilirsiniz.

2. WebGL'ye arka arabellekte alfa istemediğinizi bildirin

gl = canvas.getContext("experimental-webgl", {alpha: false});

Bu, arka arabellekte yalnızca RGB olacağından OpenGL gibi davranmasını sağlar. İyi bir tarayıcı, alfa değerinizin olmadığını anlayıp WebGL'nin birleştirilme şeklini optimize edebileceğinden bu muhtemelen en iyi seçenektir. Elbette bu, arka arabellekte alfa olmayacağı anlamına da gelir. Dolayısıyla, arka arabellekte alfayı belirli bir amaç için kullanıyorsanız bu yöntem sizin için uygun olmayabilir. Arka arabellekte alfa kullanan çok az uygulama biliyorum. Bence bu varsayılan ayar olmalıydı.

3. Oluşturma işleminizin sonunda alfayı temizleme

..
renderScene();
..
// Set the backbuffer's alpha to 1.0
gl.clearColor(1, 1, 1, 1);
gl.colorMask(false, false, false, true);
gl.clear(gl.COLOR_BUFFER_BIT);

Çoğu donanımda özel bir durum olduğu için temizleme işlemi genellikle çok hızlıdır. Demolarımın çoğunda bunu yaptım. Akıllıca bir şey yapmak istiyorsam yukarıdaki 2. yönteme geçmem gerekir. Belki bunu yayınladıktan hemen sonra yaparım. Çoğu WebGL kitaplığının varsayılan olarak bu yöntemi kullanması gerekir. Birleştirme efektleri için alfa kullanan az sayıdaki geliştirici bu özelliği talep edebilir. Geri kalanlar ise en iyi performansı ve en az sürprizi elde eder.

4. Alfa değerini bir kez temizleyin ve artık bu değere göre oluşturma yapmayın.

// At init time. Clear the back buffer.
gl.clearColor(1,1,1,1);
gl.clear(gl.COLOR_BUFFER_BIT);

// Turn off rendering to alpha
gl.colorMask(true, true, true, false);

Elbette kendi çerçeve arabelleklerinize oluşturma işlemi yapıyorsanız alfaya oluşturma işlemini tekrar açmanız ve ardından tuvale oluşturma işlemine geçtiğinizde tekrar kapatmanız gerekebilir.

5. Resimleri işleme

Ayrıca, alfa içeren PNG dosyalarını dokulara yüklüyorsanız varsayılan olarak alfa önceden çarpılır. Bu, çoğu oyunun kullandığı bir yöntem DEĞİLDİR. Bu davranışı önlemek istiyorsanız WebGL'ye şunu bildirmeniz gerekir:

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

6. Önceden çarpılmış alfa ile çalışan bir karıştırma denklemi kullanma

Yazdığım veya üzerinde çalıştığım neredeyse tüm OpenGL uygulamaları

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

Bu, önceden çarpılmamış alfa dokular için geçerlidir. Önceden çarpılmış alfa dokularla çalışmak istiyorsanız muhtemelen

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

Bildiğim yöntemler bunlar. Bildiğiniz başka bir yöntem varsa lütfen aşağıda paylaşın.