Dasar-Dasar WebGL

Gregg Tavares
Gregg Tavares

WebGL memungkinkan Anda menampilkan grafik 3D real-time yang luar biasa di browser, tetapi yang tidak diketahui banyak orang adalah WebGL sebenarnya adalah API 2D, bukan API 3D. Berikut ini penjelasannya.

WebGL hanya menangani 2 hal. Koordinat ruang klip dalam 2D dan warna. Tugas Anda sebagai programmer yang menggunakan WebGL adalah menyediakan 2 hal tersebut untuk WebGL. Anda menyediakan 2 "shader" untuk melakukannya. Vertex shader yang menyediakan koordinat ruang klip dan fragment shader yang menyediakan warna. Koordinat ruang klip selalu berkisar dari -1 hingga +1 terlepas dari ukuran kanvas Anda. Berikut adalah contoh WebGL sederhana yang menampilkan WebGL dalam bentuk paling sederhana.

// 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);

Berikut adalah 2 shader

<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>

Sekali lagi, koordinat ruang klip selalu berkisar dari -1 hingga +1, terlepas dari ukuran kanvas. Dalam kasus di atas, Anda dapat melihat bahwa kita tidak melakukan apa pun selain meneruskan data posisi secara langsung. Karena data posisi sudah berada di ruang klip, tidak ada pekerjaan yang harus dilakukan. Jika Anda menginginkan 3D, Anda dapat menyediakan shader yang mengonversi dari 3D ke 2D karena WebGL ADALAH API 2D. Untuk hal-hal 2D, Anda mungkin lebih suka bekerja dalam piksel daripada ruang klip, jadi mari kita ubah shader agar kita dapat menyediakan persegi panjang dalam piksel dan mengonversinya ke ruang klip untuk kita. Berikut adalah shader vertex baru

<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>

Sekarang kita dapat mengubah data dari ruang klip ke piksel

// 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);

Anda mungkin melihat persegi panjang berada di dekat bagian bawah area tersebut. WebGL menganggap sudut kiri bawah sebagai 0,0. Agar menjadi sudut kiri atas yang lebih tradisional yang digunakan untuk API grafis 2D, kita cukup membalik koordinat y.

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

Mari kita buat kode yang menentukan persegi panjang menjadi fungsi sehingga kita dapat memanggilnya untuk persegi panjang berukuran berbeda. Sementara itu, kita akan membuat warna dapat ditetapkan. Pertama, kita membuat shader fragmen mengambil input seragam warna.

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

uniform vec4 u_color;

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

Berikut adalah kode baru yang menggambar 50 persegi panjang di tempat acak dan warna acak.

...

  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);
}

Semoga Anda dapat melihat bahwa WebGL sebenarnya adalah API yang cukup sederhana. Meskipun dapat menjadi lebih rumit untuk melakukan 3D, kerumitan tersebut ditambahkan oleh Anda, programmer, dalam bentuk shader yang lebih kompleks. WebGL API itu sendiri bersifat 2D dan cukup sederhana.

Apa yang dimaksud dengan type="x-shader/x-vertex" dan type="x-shader/x-fragment"?

Tag <script> secara default berisi JavaScript. Anda dapat tidak memasukkan jenis atau memasukkan type="javascript" atau type="text/javascript" dan browser akan menafsirkan konten sebagai JavaScript. Jika Anda memasukkan hal lain, browser akan mengabaikan konten tag skrip.

Kita dapat menggunakan fitur ini untuk menyimpan shader dalam tag skrip. Yang lebih baik lagi, kita dapat membuat jenis kita sendiri dan dalam JavaScript kita mencarinya untuk memutuskan apakah akan mengompilasi shader sebagai shader verteks atau fragmen.

Dalam hal ini, fungsi createShaderFromScriptElement akan mencari skrip dengan id yang ditentukan, lalu melihat type untuk menentukan jenis shader yang akan dibuat.

Pemrosesan Gambar WebGL

Pemrosesan gambar itu mudah di WebGL. Seberapa mudah? Baca di bawah ini.

Untuk menggambar gambar di WebGL, kita perlu menggunakan tekstur. Serupa dengan cara WebGL mengharapkan koordinat ruang klip saat merender, bukan piksel, WebGL mengharapkan koordinat tekstur saat membaca tekstur. Koordinat tekstur berkisar dari 0,0 hingga 1,0 tidak peduli dimensi tekstur. Karena kita hanya menggambar satu persegi panjang (ya, 2 segitiga), kita perlu memberi tahu WebGL tempat di tekstur yang sesuai dengan setiap titik dalam persegi panjang. Kita akan meneruskan informasi ini dari shader vertex ke shader fragmen menggunakan jenis variabel khusus yang disebut 'beragam'. Ini disebut variabel karena bervariasi. WebGL akan melakukan interpolasi nilai yang kami berikan di vertex shader saat menggambar setiap piksel menggunakan fragment shader. Dengan menggunakan vertex shader dari akhir bagian sebelumnya, kita perlu menambahkan atribut untuk meneruskan koordinat tekstur, lalu meneruskannya ke shader fragmen.

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;
}

Kemudian, kita menyediakan shader fragmen untuk mencari warna dari tekstur.

<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>

Terakhir, kita perlu memuat gambar, membuat tekstur, dan menyalin gambar ke dalam tekstur. Karena kita berada di browser, gambar dimuat secara asinkron sehingga kita harus mengatur ulang kode sedikit untuk menunggu tekstur dimuat. Setelah dimuat, kita akan menggambarnya.

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);
  ...
}

Tidak terlalu menarik, jadi mari kita manipulasi gambar tersebut. Bagaimana jika kita menukar warna merah dan biru?

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

Bagaimana jika kita ingin melakukan pemrosesan gambar yang benar-benar melihat piksel lain? Karena WebGL mereferensikan tekstur dalam koordinat tekstur yang berkisar dari 0,0 hingga 1,0, kita dapat menghitung jumlah yang akan dipindahkan untuk 1 piksel dengan onePixel = 1.0 / textureSize matematika sederhana. Berikut adalah shader fragmen yang menghitung rata-rata piksel kiri dan kanan dari setiap piksel dalam tekstur.

<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>

Kemudian, kita perlu meneruskan ukuran tekstur dari JavaScript.

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

Setelah mengetahui cara mereferensikan piksel lain, mari kita gunakan kernel konvolusi untuk melakukan serangkaian pemrosesan gambar umum. Dalam hal ini, kita akan menggunakan kernel 3x3. Kernel konvolusi hanyalah matriks 3 x 3 di mana setiap entri dalam matriks mewakili berapa banyak untuk mengalikan 8 {i>pixel<i} di sekitar piksel yang kita render. Kemudian kita membagi hasilnya dengan bobot {i>kernel<i} (jumlah semua nilai dalam {i>kernel<i}) atau 1,0, mana saja yang lebih besar. Berikut artikel yang cukup bagus tentang hal ini. Dan berikut artikel lain yang menampilkan beberapa kode sebenarnya jika Anda menulisnya dengan tangan di C++. Dalam kasus ini, kita akan melakukan pekerjaan tersebut di shader, jadi berikut adalah shader fragmen baru.

<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>

Di JavaScript, kita perlu menyediakan {i> kernel<i} konvolusi.

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

Semoga ini meyakinkan Anda bahwa pemrosesan gambar di WebGL cukup sederhana. Selanjutnya, saya akan membahas cara menerapkan lebih dari satu efek ke gambar.

Apa yang dimaksud dengan awalan a, u, dan v_ di depan variabel dalam GLSL?

Itu hanya konvensi penamaan. a_ untuk atribut yang merupakan data yang disediakan oleh buffering. u_ untuk seragam yang merupakan input ke shader, v_ untuk variasi yang merupakan nilai yang diteruskan dari shader vertex ke shader fragmen dan diinterpolasi (atau divariasikan) di antara vertex untuk setiap piksel yang digambar.

Menerapkan beberapa efek

Pertanyaan berikutnya yang paling jelas untuk pemrosesan gambar adalah bagaimana cara menerapkan beberapa efek?

Nah, Anda dapat mencoba membuat shader secara langsung. Berikan UI yang memungkinkan pengguna memilih efek yang ingin digunakan, lalu buat shader yang melakukan semua efek. Hal ini mungkin tidak selalu dapat dilakukan meskipun teknik tersebut sering digunakan untuk membuat efek untuk grafis real time. Cara yang lebih fleksibel adalah menggunakan 2 tekstur lagi dan merender ke setiap tekstur secara bergantian, bolak-balik dan menerapkan efek berikutnya setiap kali.

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

Untuk melakukan ini, kita perlu membuat {i>framebuffer<i}. Di WebGL dan OpenGL, Framebuffer sebenarnya adalah nama yang buruk. Framebuffer WebGL/OpenGL sebenarnya hanyalah kumpulan status dan bukan buffer apa pun. Namun, dengan melampirkan tekstur ke framebuffer, kita dapat merender ke tekstur tersebut. Pertama, mari kita ubah kode pembuatan tekstur lama menjadi fungsi

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);

Sekarang, mari kita gunakan fungsi tersebut untuk membuat 2 tekstur lagi dan melampirkannya ke 2 framebuffer.

// 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);
}

Sekarang, mari kita buat kumpulan kernel, lalu daftar kernel yang akan diterapkan.

// 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"
];

Dan akhirnya mari kita terapkan masing-masing, melakukan ping ponging tekstur mana yang kita render juga

// 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);
}

Ada beberapa hal yang harus saya bahas.

Memanggil gl.bindFramebuffer dengan null memberi tahu WebGL bahwa Anda ingin merender ke kanvas, bukan ke salah satu framebuffer. WebGL harus mengonversi dari ruang klip kembali menjadi piksel. Hal ini dilakukan berdasarkan setelan gl.viewport. Setelan gl.viewport secara default adalah ukuran kanvas saat kita melakukan inisialisasi WebGL. Karena framebuffer yang kita render memiliki ukuran yang berbeda dengan kanvas, kita perlu menetapkan area pandang dengan tepat. Terakhir, dalam contoh dasar-dasar WebGL, kita membalik koordinat Y saat merender karena WebGL menampilkan kanvas dengan 0,0 sebagai sudut kiri bawah, bukan sudut kiri atas 2D yang lebih tradisional. Hal itu tidak diperlukan saat merender ke framebuffer. Karena framebuffer tidak pernah ditampilkan, bagian mana yang merupakan bagian atas dan bawah tidak relevan. Yang penting adalah piksel 0,0 dalam framebuffer sesuai dengan 0,0 dalam perhitungan kami. Untuk mengatasi hal ini, saya memungkinkan untuk menetapkan apakah akan membalik atau tidak dengan menambahkan satu input lagi ke shader.

<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>

Dan kemudian kita dapat menyetelnya ketika kita merender dengan

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

Saya membuat contoh ini tetap sederhana dengan menggunakan satu program GLSL yang dapat mencapai beberapa efek. Jika ingin melakukan pemrosesan gambar secara menyeluruh, Anda mungkin memerlukan banyak program GLSL. Program untuk penyesuaian hue, saturasi, dan luminans. Satu lagi untuk kecerahan dan kontras. Satu untuk membalik, satu lagi untuk menyesuaikan level, dll. Anda harus mengubah kode untuk mengalihkan program GLSL dan memperbarui parameter untuk program tertentu tersebut. Saya telah mempertimbangkan untuk menulis contoh tersebut, tetapi sebaiknya latihan ini diserahkan kepada pembaca karena beberapa program GLSL yang masing-masing memiliki kebutuhan parameternya sendiri mungkin berarti beberapa pemfaktoran ulang besar untuk mencegah semuanya menjadi berantakan. Semoga contoh ini dan contoh sebelumnya telah membuat WebGL tampak sedikit lebih mudah dipahami dan semoga memulai dengan 2D membantu membuat WebGL sedikit lebih mudah dipahami. Jika saya menemukan waktu yang tepat, saya akan mencoba menulis beberapa artikel lagi tentang cara melakukan 3D serta detail selengkapnya tentang apa yang sebenarnya dilakukan WebGL di balik layar.

WebGL dan Alfa

Saya telah melihat beberapa developer OpenGL mengalami masalah dengan cara WebGL memperlakukan alfa di backbuffer (yaitu kanvas), jadi saya pikir sebaiknya kita membahas beberapa perbedaan antara WebGL dan OpenGL yang terkait dengan alfa.

Perbedaan terbesar antara OpenGL dan WebGL adalah OpenGL dirender ke backbuffer yang tidak digabungkan dengan apa pun, atau secara efektif tidak digabungkan dengan apa pun oleh pengelola jendela OS, sehingga tidak masalah apa pun alfa Anda. WebGL digabungkan oleh browser dengan halaman web dan defaultnya adalah menggunakan alfa yang telah dikalikan sebelumnya sama seperti tag <img> .png dengan transparansi dan tag kanvas 2D. WebGL memiliki beberapa cara untuk membuatnya lebih mirip dengan OpenGL.

#1) Beri tahu WebGL bahwa Anda ingin mengkomposisinya dengan alfa non-premultiplied

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

Nilai defaultnya adalah benar (true). Tentu saja hasilnya akan tetap digabungkan di seluruh halaman dengan warna latar belakang apa pun yang berada di bawah kanvas (warna latar belakang kanvas, warna latar belakang penampung kanvas, warna latar belakang halaman, hal-hal di belakang kanvas jika kanvas memiliki z-index > 0, dll.) dengan kata lain, warna yang ditentukan CSS untuk area halaman web tersebut. Cara yang sangat baik untuk mengetahui apakah Anda memiliki masalah alfa adalah dengan menetapkan latar belakang kanvas ke warna cerah seperti merah. Anda akan langsung melihat apa yang terjadi.

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

Anda juga dapat menyetelnya ke hitam yang akan menyembunyikan masalah alfa yang Anda miliki.

#2) Beri tahu WebGL bahwa Anda tidak ingin alpha di backbuffer

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

Hal ini akan membuatnya berperilaku lebih seperti OpenGL karena backbuffer hanya akan memiliki RGB. Ini mungkin opsi terbaik karena browser yang baik dapat melihat bahwa Anda tidak memiliki alfa dan benar-benar mengoptimalkan cara WebGL digabungkan. Tentu saja, hal ini juga berarti bahwa alpha tidak akan ada di backbuffer, jadi jika Anda menggunakan alpha di backbuffer untuk tujuan tertentu, hal itu mungkin tidak berfungsi untuk Anda. Beberapa aplikasi yang saya tahu menggunakan alfa di {i>backbuffer<i}. Saya rasa ini seharusnya menjadi setelan default.

#3) Menghapus alfa di akhir rendering

..
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);

Pembersihan biasanya sangat cepat karena ada kasus khusus untuknya di sebagian besar hardware. Saya melakukannya di sebagian besar demo saya. Jika saya pintar, saya akan beralih ke metode #2 di atas. Mungkin saya akan melakukannya tepat setelah memposting ini. Sepertinya sebagian besar library WebGL akan menggunakan metode ini secara default. Beberapa developer yang benar-benar menggunakan alfa untuk efek komposisi dapat memintanya. Sisanya hanya akan mendapatkan performa terbaik dan kejutan paling sedikit.

#4) Hapus alfa sekali lalu jangan render ke alfa lagi

// 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);

Tentu saja jika Anda melakukan rendering ke framebuffer sendiri, Anda mungkin perlu mengaktifkan kembali rendering ke alfa lalu menonaktifkannya lagi saat beralih ke rendering ke kanvas.

#5) Menangani Gambar

Selain itu, jika Anda memuat file PNG dengan alfa ke dalam tekstur, default-nya adalah alfa yang telah dikalikan sebelumnya, yang umumnya BUKAN cara sebagian besar game melakukan hal-hal. Jika ingin mencegah perilaku tersebut, Anda harus memberi tahu WebGL dengan

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

#6) Menggunakan persamaan penggabungan yang berfungsi dengan alfa yang telah dikalikan sebelumnya

Hampir semua aplikasi OpenGL yang saya tulis atau kerjakan menggunakan

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

Hal ini berfungsi untuk tekstur alfa yang tidak dihitung sebelumnya. Jika Anda benar-benar ingin menggunakan tekstur alfa yang telah dikalikan sebelumnya, Anda mungkin ingin

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

Itu adalah metode yang saya ketahui. Jika Anda mengetahui lebih banyak, kirimkan di bawah.