مبانی WebGL

Gregg Tavares
Gregg Tavares

WebGL امکان نمایش گرافیک‌های سه‌بعدی بی‌نظیر را در مرورگر شما فراهم می‌کند، اما چیزی که خیلی‌ها نمی‌دانند این است که WebGL درواقع یک میانای برنامه‌سازی کاربردی دوبعدی است، نه سه‌بعدی. بگذارید توضیح دهم.

WebGL فقط به ۲ چیز اهمیت می‌دهد. مختصات فضای برش در حالت دوبعدی و رنگ‌ها. وظیفه شما به‌عنوان برنامه‌نویسی که از WebGL استفاده می‌کند این است که این ۲ چیز را برای WebGL فراهم کنید. برای انجام این کار، ۲ «سایه‌زن» ارائه می‌دهید. یک سایه‌زن Vertex که مختصات فضای برش را ارائه می‌دهد و یک سایه‌زن قطعه که رنگ را ارائه می‌دهد. مختصات Clipspace همیشه از ۱- تا ۱+ است، صرف‌نظر از اینکه اندازه بوم شما چقدر باشد. در اینجا یک مثال ساده WebGL وجود دارد که WebGL را در ساده‌ترین شکل خود نشان می‌دهد.

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

این هم از ۲ سایه‌زن

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

دوباره، مختصات فضای برش همیشه از ‎-1 تا ‎+1 است، صرف‌نظر از اندازه بوم. در مورد بالا می‌بینید که ما کاری جز انتقال مستقیم داده‌های موقعیت مکانی‌مان انجام نمی‌دهیم. ازآنجایی‌که داده‌های موقعیت ازقبل در فضای برش قرار دارند، کاری برای انجام دادن وجود ندارد. اگر سه‌بعدی می‌خواهید، این به شما بستگی دارد که سایه‌زن‌هایی را ارائه دهید که از سه‌بعدی به دوبعدی تبدیل می‌شوند، زیرا WebGL یک میانای برنامه‌سازی کاربردی دوبعدی است! برای چیزهای دوبعدی احتمالاً ترجیح می‌دهید به‌جای فضای برش در پیکسل کار کنید، بنابراین بیایید سایه‌زن را تغییر دهیم تا بتوانیم مستطیل‌ها را در پیکسل ارائه دهیم و آن را برای ما به فضای برش تبدیل کنیم. این سایه‌زن رأس جدید است

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

اکنون می‌توانیم داده‌هایمان را از فضای برش به پیکسل تغییر دهیم

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

ممکن است متوجه شوید که مستطیل نزدیک به پایین آن ناحیه است. WebGL گوشه پایین سمت چپ را 0,0 درنظر می‌گیرد. برای اینکه آن را به گوشه بالا سمت چپ سنتی‌تر که برای میاناهای برنامه‌سازی کاربردی گرافیک دوبعدی استفاده می‌شود تبدیل کنیم، فقط مختصات y را برعکس می‌کنیم.

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

بیایید کدی را که مستطیل را تعریف می‌کند به تابع تبدیل کنیم تا بتوانیم آن را برای مستطیل‌هایی با اندازه‌های مختلف فراخوانی کنیم. درحالی‌که در آن هستیم، رنگ را قابل تنظیم می‌کنیم. ابتدا سایه‌زن قطعه را طوری تنظیم می‌کنیم که ورودی یکنواخت رنگ را دریافت کند.

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

uniform vec4 u_color;

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

و این کد جدیدی است که ۵۰ مستطیل را در مکان‌های تصادفی و با رنگ‌های تصادفی ترسیم می‌کند.

...

  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 درواقع میانای برنامه‌سازی کاربردی بسیار ساده‌ای است. اگرچه انجام سه‌بعدی می‌تواند پیچیده‌تر شود، اما این پیچیدگی توسط شما، برنامه‌نویس، در قالب سایه‌زن‌های پیچیده‌تر اضافه می‌شود. خود WebGL API دوبعدی و نسبتاً ساده است.

‫type="x-shader/x-vertex"‎ و type="x-shader/x-fragment"‎ به چه معنا هستند؟

برچسب‌های <script> به‌طور پیش‌فرض حاوی جاوا اسکریپت هستند. می‌توانید هیچ نوعی قرار ندهید یا می‌توانید type="javascript" یا type="text/javascript" قرار دهید و مرورگر محتوا را به‌عنوان «جاوا اسکریپت» تفسیر خواهد کرد. اگر چیز دیگری قرار دهید، مرورگر محتوای برچسب دستورگان را نادیده می‌گیرد.

می‌توانیم از این ویژگی برای ذخیره کردن سایه‌زن‌ها در برچسب‌های دستورگان استفاده کنیم. حتی بهتر از آن، می‌توانیم نوع خودمان را بسازیم و در جاوا اسکریپت خود به‌دنبال آن بگردیم تا تصمیم بگیریم که آیا سایه‌زن را به‌عنوان سایه‌زن رأس یا سایه‌زن قطعه کامپایل کنیم.

در این حالت، تابع createShaderFromScriptElement به‌دنبال اسکریپتی با id مشخص‌شده می‌گردد و سپس به type نگاه می‌کند تا تصمیم بگیرد چه نوع سایه‌زنی ایجاد کند.

پردازش تصویر WebGL

پردازش تصویر در WebGL آسان است. چقدر آسان؟ در زیر بخوانید.

برای طراحی تصاویر در WebGL باید از بافت‌ها استفاده کنیم. به‌طور مشابه با روشی که WebGL هنگام رندر کردن به‌جای پیکسل‌ها مختصات فضای برش را انتظار دارد، WebGL هنگام خواندن بافت، مختصات بافت را انتظار دارد. مختصات بافت از ۰٫۰ تا ۱٫۰ است، صرف‌نظر از ابعاد بافت. ازآنجایی‌که فقط یک مستطیل (درواقع ۲ مثلث) رسم می‌کنیم، باید به WebGL بگوییم که هر نقطه در مستطیل با کدام مکان در بافت مطابقت دارد. این اطلاعات را از سایه‌زن رأس به سایه‌زن قطعه بااستفاده از نوع خاصی از متغیر به‌نام «متغیر» منتقل می‌کنیم. به آن متغیر می‌گویند چون تغییر می‌کند. WebGL مقادیری را که در سایه‌زن رأس ارائه می‌دهیم، درحالی‌که هر پیکسل را بااستفاده از سایه‌زن قطعه ترسیم می‌کند، درون‌یابی خواهد کرد. بااستفاده از سایه‌زن رأس از انتهای بخش قبلی، باید مشخصه‌ای اضافه کنیم تا مختصات بافت را ارسال کند و سپس آن‌ها را به سایه‌زن قطعه ارسال کند.

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

سپس یک سایه‌زن قطعه‌ای ارائه می‌دهیم تا رنگ‌ها را از بافت جستجو کند.

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

درنهایت باید تصویری را بار کنیم، بافتی ایجاد کنیم، و تصویر را در بافت کپی کنیم. چون در مرورگر هستیم، تصاویر به‌صورت ناهمگام بار می‌شوند، بنابراین باید کد را کمی بازآرایی کنیم تا منتظر بار شدن بافت بمانیم. وقتی بار شد آن را می‌کشیم.

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

خیلی هیجان‌انگیز نیست، پس بیایید آن تصویر را دستکاری کنیم. نظرت درباره جابه‌جا کردن قرمز و آبی چیست؟

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

اگر بخواهیم پردازش تصویری انجام دهیم که واقعاً به پیکسل‌های دیگر نگاه کند، چه؟ ازآنجایی‌که WebGL به بافت‌ها در مختصات بافت که از ۰٫۰ تا ۱٫۰ می‌رود ارجاع می‌دهد، می‌توانیم با ریاضی ساده onePixel = 1.0 / textureSize محاسبه کنیم که چقدر باید برای ۱ پیکسل حرکت کنیم. در اینجا یک سایه‌زن قطعه‌ای وجود دارد که میانگین پیکسل‌های چپ و راست هر پیکسل را در بافت محاسبه می‌کند.

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

سپس باید اندازه بافت را از جاوا اسکریپت عبور دهیم.

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

اکنون که می‌دانیم چگونه به پیکسل‌های دیگر ارجاع دهیم، بیایید از هسته پیچشی برای انجام یک‌سری پردازش‌های رایج تصویر استفاده کنیم. در این مورد، از هسته ۳×۳ استفاده می‌کنیم. هسته پیچش فقط یک ماتریس ۳×۳ است که هر ورودی در ماتریس نشان می‌دهد که ۸ پیکسل اطراف پیکسلی که درحال پردازش آن هستیم باید چقدر ضرب شود. سپس نتیجه را بر وزن هسته (مجموع همه مقادیر در هسته) یا ۱٫۰ (هرکدام که بزرگ‌تر باشد) تقسیم می‌کنیم. این مقاله خوبی درباره آن است. و این مقاله دیگری است که کدی واقعی را نشان می‌دهد که اگر بخواهید آن را به‌صورت دستی در C++ بنویسید. در مورد ما، این کار را در سایه‌زن انجام خواهیم داد، بنابراین این سایه‌زن قطعه جدید است.

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

در جاوا اسکریپت باید یک هسته پیچشی ارائه دهیم.

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

امیدوارم این موضوع شما را متقاعد کرده باشد که پردازش تصویر در WebGL بسیار ساده است. در ادامه، نحوه اعمال بیش‌از یک جلوه روی تصویر را توضیح خواهم داد.

پیشوندهای a، u، و v_ در متغیرهای GLSL چه معنایی دارند؟

این فقط یک قرارداد نام‌گذاری است. a_ برای مشخصه‌هایی که داده‌های ارائه‌شده توسط میان‌گیرها هستند. u_ برای یونیفرم‌هایی که ورودی‌های سایه‌زن‌ها هستند، v_ برای متغیرهایی که مقادیر منتقل‌شده از سایه‌زن رأس به سایه‌زن قطعه هستند و بین رأس‌ها برای هر پیکسل ترسیم‌شده درون‌یابی (یا متغیر) می‌شوند.

درحال اعمال چند جلوه

سؤال واضح بعدی برای پردازش تصویر این است که چگونه چندین جلوه را اعمال کنیم؟

خب، می‌توانید سایه‌زن‌ها را درحین اجرا تولید کنید. رابط کاربری‌ای ارائه دهید که به کاربر اجازه دهد جلوه‌هایی را که می‌خواهد استفاده کند انتخاب کند، سپس سایه‌زنی تولید کنید که همه جلوه‌ها را انجام دهد. ممکن است همیشه امکان‌پذیر نباشد، هرچند این تکنیک اغلب برای ایجاد جلوه‌هایی برای گرافیک‌های هم‌زمان استفاده می‌شود. روش انعطاف‌پذیرتر این است که از ۲ بافت دیگر استفاده کنید و به‌نوبت در هر بافت رندر کنید، به‌صورت پینگ‌پنگی عقب و جلو بروید و هربار جلوه بعدی را اعمال کنید.

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

برای این کار باید میانگیرهای قاب ایجاد کنیم. در WebGL و OpenGL، «بافر قاب» درواقع نام مناسبی نیست. «میان‌گیر قاب» WebGL/OpenGL درواقع فقط مجموعه‌ای از وضعیت است و درواقع هیچ نوع میان‌گیری نیست. اما با پیوست کردن بافت به میانگیر قاب می‌توانیم در آن بافت رندر کنیم. ابتدا بیایید کد قدیمی ایجاد بافت را به تابع تبدیل کنیم

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

و حالا بیایید از آن تابع برای ساختن ۲ بافت دیگر و پیوست کردن آن‌ها به ۲ میان‌گیر قاب استفاده کنیم.

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

اکنون بیایید مجموعه‌ای از هسته‌ها و سپس فهرستی از آن‌ها را برای اعمال کردن ایجاد کنیم.

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

و درنهایت بیایید هرکدام را اعمال کنیم، و بافت‌هایی را که رندر می‌کنیم پینگ‌پنگ کنیم

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

چند چیز هست که باید بررسی کنم.

فراخوانی gl.bindFramebuffer با null به WebGL می‌گوید که می‌خواهید به‌جای یکی از میانگیرهای قاب، در بوم رندر کنید. WebGL باید از فضای برش به پیکسل تبدیل شود. این کار را براساس تنظیمات gl.viewport انجام می‌دهد. تنظیمات gl.viewport به‌طور پیش‌فرض روی اندازه بوم است وقتی WebGL را مقداردهی اولیه می‌کنیم. ازآنجایی‌که میانگیرهای قابی که در آن‌ها رندر می‌کنیم اندازه متفاوتی با بوم دارند، باید ناحیه نمایش را به‌درستی تنظیم کنیم. درنهایت، در مثال‌های اصول WebGL، مختصات Y را هنگام رندر کردن برگرداندیم زیرا WebGL بوم نقاشی را با 0,0 به‌عنوان گوشه پایین سمت چپ به‌جای گوشه بالا سمت چپ سنتی‌تر برای 2D نمایش می‌دهد. هنگام رندر کردن در میانگیر قاب، این کار لازم نیست. چون میان‌گیر قاب هرگز نمایش داده نمی‌شود، اینکه کدام بخش بالا و کدام بخش پایین است اهمیتی ندارد. تنها چیزی که اهمیت دارد این است که پیکسل 0,0 در میانگیر قاب با 0,0 در محاسبات ما مطابقت داشته باشد. برای حل این مشکل، با افزودن یک ورودی دیگر به سایه‌زن، امکان تنظیم کردن اینکه وارونه شود یا نه را فراهم کردم.

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

And then we can set it when we render with

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

من این مثال را بااستفاده از یک برنامه GLSL که می‌تواند چندین جلوه را به‌دست آورد، ساده نگه داشتم. اگر می‌خواستید پردازش کامل تصویر را انجام دهید، احتمالاً به برنامه‌های GLSL زیادی نیاز داشتید. برنامه‌ای برای تنظیم فام، اشباع رنگ، و درخشندگی. و دیگری برای روشنایی و کنتراست. یکی برای معکوس کردن، دیگری برای تنظیم سطوح، و غیره. برای جابه‌جایی برنامه‌های GLSL و به‌روزرسانی پارامترهای آن برنامه خاص، باید کد را تغییر دهید. I'd considered writing that example but it's an exercise best left to the reader because multiple GLSL programs each with their own parameter needs probably means some major refactoring to keep it all from becoming a big mess of spaghetti. امیدوارم این مثال و مثال‌های قبلی باعث شده باشد WebGL کمی دردسترس‌تر به‌نظر برسد و امیدوارم شروع با 2D کمک کند WebGL کمی آسان‌تر درک شود. اگر وقت کنم، سعی می‌کنم چند مقاله دیگر درباره نحوه انجام سه‌بعدی و همچنین جزئیات بیشتری درباره آنچه WebGL واقعاً در پشت صحنه انجام می‌دهد بنویسم.

WebGL و آلفا

متوجه شده‌ام که برخی‌از توسعه‌دهندگان OpenGL با نحوه برخورد WebGL با آلفا در میان‌گیر پشتیبان (یعنی بوم) مشکل دارند، بنابراین فکر کردم خوب است که برخی‌از تفاوت‌های بین WebGL و OpenGL مربوط به آلفا را بررسی کنیم.

بزرگ‌ترین تفاوت بین OpenGL و WebGL این است که OpenGL در یک میان‌گیر پشتی پردازش می‌کند که با هیچ‌چیز ترکیب نمی‌شود، یا درواقع توسط مدیر پنجره سیستم‌عامل با هیچ‌چیز ترکیب نمی‌شود، بنابراین مهم نیست که آلفای شما چیست. WebGL توسط مرورگر با صفحه وب ترکیب می‌شود و پیش‌فرض این است که از آلفای پیش‌ضرب‌شده مانند برچسب‌های <img> .png با شفافیت و برچسب‌های بوم ۲بعدی استفاده شود. WebGL چندین روش برای شبیه‌تر کردن این به OpenGL دارد.

#1. به WebGL بگویید که می‌خواهید با آلفای غیرپیش‌ضرب‌شده ترکیب شود

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

پیش‌فرض «درست» است. البته نتیجه همچنان با هر رنگ پس‌زمینه‌ای که درنهایت زیر بوم قرار می‌گیرد (رنگ پس‌زمینه بوم، رنگ پس‌زمینه محفظه بوم، رنگ پس‌زمینه صفحه، چیزهای پشت بوم اگر بوم دارای z-index > 0 باشد، و غیره…) روی صفحه ترکیب می‌شود، به عبارت دیگر، رنگی که CSS برای آن ناحیه از صفحه وب تعریف می‌کند. یک راه واقعاً خوب برای اینکه ببینید آیا مشکل آلفا دارید یا نه این است که پس‌زمینه بوم را روی رنگ روشنی مثل قرمز تنظیم کنید. فوراً خواهید دید چه اتفاقی درحال رخ دادن است.

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

همچنین می‌توانید آن را روی سیاه تنظیم کنید که هرگونه مشکل آلفایی را که دارید پنهان می‌کند.

#2. به WebGL بگویید که آلفا را در بافر پشتی نمی‌خواهید

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

این کار باعث می‌شود که بیشتر شبیه OpenGL عمل کند زیرا میان‌گیر پشتی فقط RGB خواهد داشت. این احتمالاً بهترین گزینه است زیرا یک مرورگر خوب می‌تواند ببیند که شما آلفا ندارید و درواقع نحوه ترکیب WebGL را بهینه‌سازی کند. البته این بدان معنی است که درواقع آلفا در میانگیر پشتی وجود نخواهد داشت، بنابراین اگر از آلفا در میانگیر پشتی برای هدفی استفاده می‌کنید، ممکن است برای شما کار نکند. Few apps that I know of use alpha in the backbuffer. فکر می‌کنم این احتمالاً باید پیش‌فرض می‌بود.

#3. پاک کردن آلفا در پایان پرداز زدن

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

پاک کردن معمولاً بسیار سریع است زیرا در اکثر سخت‌افزارها مورد خاصی برای آن وجود دارد. این کار را در بیشتر نسخه‌های نمایشی‌ام انجام دادم. اگر باهوش بودم، به روش شماره ۲ در بالا می‌رفتم. شاید بلافاصله بعداز پست کردن این، آن کار را انجام دهم. به‌نظر می‌رسد اکثر کتابخانه‌های WebGL باید به‌طور پیش‌فرض از این روش استفاده کنند. آن دسته از توسعه‌دهندگانی که واقعاً از آلفا برای جلوه‌های ترکیب‌بندی استفاده می‌کنند می‌توانند آن را درخواست کنند. بقیه فقط بهترین عملکرد و کمترین غافلگیری را دریافت خواهند کرد.

#4. آلفا را یک‌بار پاک کنید و دیگر آن را رندر نکنید

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

البته اگر درحال پرداز به میانگیرهای قاب خودتان هستید، ممکن است لازم باشد پرداز به آلفا را دوباره روشن کنید و سپس وقتی به پرداز به بوم تغییر می‌دهید آن را دوباره خاموش کنید.

#5. مدیریت تصاویر

همچنین، اگر فایل‌های PNG را با آلفا در بافت‌ها بار می‌کنید، به‌طور پیش‌فرض آلفای آن‌ها ازقبل ضرب شده است که معمولاً روشی نیست که اکثر بازی‌ها انجام می‌دهند. اگر می‌خواهید از این رفتار جلوگیری کنید، باید به WebGL بگویید

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

#6. استفاده از معادله ترکیب که با آلفای ازپیش‌ضرب‌شده کار می‌کند

تقریباً همه برنامه‌های OpenGL که نوشته‌ام یا روی آن‌ها کار کرده‌ام از

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

این برای بافت‌های آلفای غیرپیش‌ضرب‌شده کار می‌کند. اگر واقعاً می‌خواهید با بافت‌های آلفای پیش‌ضرب‌شده کار کنید، احتمالاً می‌خواهید

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

این‌ها روش‌هایی است که من می‌شناسم. اگر موارد بیشتری می‌شناسید، لطفاً آن‌ها را در زیر پست کنید.