اصول WebGL

گرگ تاوارس
Gregg Tavares

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

WebGL فقط به 2 چیز اهمیت می دهد. مختصات Clipspace به صورت دو بعدی و رنگی. وظیفه شما به عنوان یک برنامه نویس با استفاده از WebGL این است که این 2 مورد را در اختیار WebGL قرار دهید. برای انجام این کار، 2 "سایدر" ارائه می دهید. یک سایه زن Vertex که مختصات فضای کلیپ را فراهم می کند و یک سایه زن قطعه که رنگ را فراهم می کند. مختصات Clipspace همیشه از 1- به 1+ می‌رود بدون توجه به اندازه بوم شما. در اینجا یک مثال ساده 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);

در اینجا 2 سایه زن هستند

<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 یک API دو بعدی است! برای چیزهای دو بعدی احتمالاً ترجیح می دهید در پیکسل کار کنید تا فضای کلیپ، بنابراین بیایید سایه زن را تغییر دهیم تا بتوانیم مستطیل ها را در پیکسل عرضه کنیم و آن را به فضای کلیپ برای ما تبدیل کنیم. در اینجا سایه زن راس جدید است

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

و در اینجا کد جدیدی است که 50 مستطیل را در مکان های تصادفی و رنگ های تصادفی ترسیم می کند.

...

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

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

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

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

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

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

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

برای ترسیم تصاویر در WebGL باید از بافت ها استفاده کنیم. مشابه روشی که WebGL هنگام رندر کردن به جای پیکسل ها انتظار مختصات فضای کلیپ را دارد، WebGL هنگام خواندن یک بافت انتظار مختصات بافت را دارد. مختصات بافت بدون توجه به ابعاد بافت از 0.0 به 1.0 می‌رود. از آنجایی که ما فقط یک مستطیل می کشیم (خوب، 2 مثلث)، باید به WebGL بگوییم که هر نقطه از مستطیل با کدام مکان بافت مطابقت دارد. ما این اطلاعات را با استفاده از نوع خاصی از متغیر به نام "متغیر" از سایه‌زن راس به سایه‌زن قطعه ارسال می‌کنیم. به آن متغیر می گویند زیرا متفاوت است. WebGL مقادیری را که ما در سایه‌زن راس ارائه می‌دهیم درون‌یابی می‌کند، زیرا هر پیکسل را با استفاده از سایه‌زن قطعه ترسیم می‌کند. با استفاده از سایه‌زن رأس از انتهای بخش قبل، باید یک ویژگی برای ارسال مختصات بافت اضافه کنیم و سپس آن‌ها را به shader قطعه منتقل کنیم.

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 به بافت ها در مختصات بافت ارجاع می دهد که از 0.0 به 1.0 می روند، پس می توانیم با ریاضی ساده onePixel = 1.0 / textureSize محاسبه کنیم که برای 1 پیکسل چقدر حرکت کنیم. در اینجا یک قطعه سایه زن وجود دارد که میانگین پیکسل های چپ و راست هر پیکسل در بافت را نشان می دهد.

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

اکنون که می دانیم چگونه به پیکسل های دیگر ارجاع دهیم، اجازه دهید از یک هسته کانولوشن برای انجام یکسری پردازش تصویر رایج استفاده کنیم. در این مورد ما از یک هسته 3x3 استفاده می کنیم. یک هسته کانولوشن فقط یک ماتریس 3x3 است که هر ورودی در ماتریس نشان دهنده میزان ضرب 8 پیکسل در اطراف پیکسلی است که ما ارائه می کنیم. سپس نتیجه را بر وزن هسته (مجموع همه مقادیر موجود در هسته) یا 1.0 تقسیم می کنیم که هر کدام بیشتر باشد. در اینجا یک مقاله بسیار خوب در مورد آن وجود دارد . و در اینجا یک مقاله دیگر وجود دارد که برخی از کدهای واقعی را نشان می دهد اگر بخواهید این را با دست در 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_ برای متغیرهایی که مقادیری هستند که از سایه‌زن رأس به سایه‌زن قطعه ارسال می‌شوند و بین راس‌ها برای هر پیکسل ترسیم شده درون‌یابی (یا متغیر) هستند.

اعمال افکت های متعدد

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

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

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، Framebuffer در واقع نام ضعیفی است. یک فریم بافر 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);

و حالا بیایید از این تابع برای ساختن 2 بافت دیگر و وصل کردن آنها به 2 فریم بافر استفاده کنیم.

// 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 انجام می دهد. هنگامی که WebGL را مقداردهی اولیه می کنیم، تنظیمات gl.viewport به طور پیش فرض به اندازه بوم است. از آنجایی که فریم‌بافرهایی که ما در آنها رندر می‌کنیم اندازه‌های متفاوتی دارند، پس بوم باید نمای پورت را به درستی تنظیم کنیم. در نهایت در مثال‌های بنیادی WebGL، مختصات Y را هنگام رندر برگردانیدیم، زیرا WebGL بوم را با 0.0 نشان می‌دهد که در گوشه سمت چپ پایین به جای سنتی‌تر برای دو بعدی بالا سمت چپ است. در هنگام رندر کردن به فریم بافر به آن نیازی نیست. از آنجایی که فریم بافر هرگز نمایش داده نمی شود، قسمت بالا و پایین آن بی ربط است. تنها چیزی که اهمیت دارد این است که پیکسل 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>

و سپس می توانیم آن را هنگام رندر کردن با آن تنظیم کنیم

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

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

WebGL و آلفا

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

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

#1) به WebGL بگویید که می خواهید آن را با آلفای غیرقبل ضرب شده ترکیب کنید

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

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

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

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

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

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

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

شماره 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);

پاک کردن به طور کلی بسیار سریع است، زیرا یک مورد خاص برای آن در اکثر سخت افزارها وجود دارد. من این کار را در اکثر دموهای خود انجام دادم. اگر باهوش بودم به روش شماره 2 در بالا تغییر می کردم. شاید بلافاصله پس از ارسال این مطلب، این کار را انجام دهم. به نظر می رسد که اکثر کتابخانه های 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);

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