أساسيات WebGL

أساسيات WebGL

تتيح تقنية WebGL عرض رسومات ثلاثية الأبعاد مذهلة في الوقت الفعلي في المتصفّح، ولكن ما لا يعرفه الكثير من المستخدمين هو أنّ WebGL هي في الواقع واجهة برمجة تطبيقات ثنائية الأبعاد، وليست واجهة برمجة تطبيقات ثلاثية الأبعاد. ‏‫دعنا نوضّح لك.

لا يهتم WebGL إلا بعنصرَين. إحداثيات مساحة القصاصة في تنسيق ثنائي الأبعاد والألوان إنّ وظيفتك كمبرمِج يستخدم WebGL هي تزويد WebGL بهذين الشيئَين. ويجب تقديم مادّتَي "تظليل" لإجراء ذلك. برنامج تشفير قمة يقدّم إحداثيات مساحة القصاصة وبرنامج تشفير جزء يقدّم اللون تتراوح دائمًا إحداثيات مساحة القصاصة من -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);

في ما يلي المخطّطَان اللونيان

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

في ما يلي الرمز الجديد الذي يرسم 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 هي في الواقع واجهة برمجة تطبيقات بسيطة جدًا. على الرغم من أنّ إنشاء المحتوى الثلاثي الأبعاد قد يكون أكثر تعقيدًا، إلا أنّك أنت المبرمج تضيف هذا التعقيد في شكل أدوات تظليل أكثر تعقيدًا. واجهة برمجة التطبيقات WebGL نفسها ثنائية الأبعاد وبسيطة إلى حدٍ كبير.

ما هو المقصود بـ type="x-shader/x-vertex" وtype="x-shader/x-fragment"؟

تكون علامات <script> مزوّدة تلقائيًا برمز JavaScript. يمكنك عدم إدخال أي نوع أو إدخال type="javascript" أو type="text/javascript" وسيفسر المتصفّح المحتوى على أنّه JavaScript. وإذا وضعت أيّ محتوى آخر، يتجاهل المتصفّح محتوى علامة النص البرمجي.

يمكننا استخدام هذه الميزة لتخزين مواد التشويش في علامات النصوص البرمجية. والأفضل من ذلك، يمكننا إنشاء نوعنا الخاص والبحث عنه في JavaScript لتحديد ما إذا كان سيتم تجميع برنامج التظليل كبرنامج تظليل لرؤوس المضلّعات أو برنامج تظليل لشرائح المضلّعات.

في هذه الحالة، تبحث الدالة createShaderFromScriptElement عن نص برمجي يحتوي على id محدّد، ثم تبحث في type لتحديد نوع تأثير التمويه الذي سيتم إنشاؤه.

معالجة الصور باستخدام WebGL

يمكن معالجة الصور بسهولة في WebGL. ما مدى سهولة ذلك؟ يُرجى الاطّلاع على ما يلي.

لرسم الصور في WebGL، يجب استخدام مواد النسيج. على غرار الطريقة التي يتوقّع بها WebGL إحداثيات مساحة القصاصة عند العرض بدلاً من البكسل، يتوقّع WebGL إحداثيات النسيج عند قراءة نسيج. تتراوح إحداثيات النسيج من 0.0 إلى 1.0 بغض النظر عن أبعاد النسيج. بما أنّنا نرسم مستطيلاً واحدًا فقط (أو مثلثَين)، علينا إخبار WebGL بالمكان في النسيج الذي تتوافق معه كل نقطة في المستطيل. سنُرسل هذه المعلومات من برنامج تشفير قمة المثلث إلى برنامج تشفير القطعة باستخدام نوع خاص من المتغيّرات يُسمى "متغيّر متغير". ويُطلق عليه اسم متغيّر لأنّه يتغيّر. ستتمّ إضافة القيم التي نقدّمها في برنامج Shader الخاص بالرأس إلى برنامج Shader الخاص بالقطعة أثناء رسم كلّ بكسل باستخدام برنامج 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. في ما يلي برنامج تشويش أجزاء يحسب متوسط وحدات البكسل اليمنى واليسرى لكل بكسل في النسيج.

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

بعد ذلك، علينا ضبط حجم النسيج من JavaScript.

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

الآن بعد أن عرفنا كيفية الإشارة إلى وحدات البكسل الأخرى، لنستخدم نواة تصفية تلافية لإجراء مجموعة من عمليات معالجة الصور الشائعة. في هذه الحالة، سنستخدم نواة 3×3. إنّ نواة التفاف هي مجرد مصفوفة 3×3 يمثّل كل إدخال فيها مقدار ضرب وحدات البكسل الثمانية حول البكسل الذي نعرضه. ثم نقسم الناتج على وزن النواة (مجموع كل القيم في النواة) أو 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>

في JavaScript، نحتاج إلى توفير نواة تصفية تلافية.

...
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، لا يُعدّ 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);

والآن، لنستخدم هذه الدالة لإنشاء نسيجَين إضافيَين وإرفاقها بإطارَي عرض.

// 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 في أسفل يمين الشاشة بدلاً من أعلى يمين الشاشة في التنسيق ثنائي الأبعاد الأكثر شيوعًا. ولا يلزم ذلك عند التصدير إلى إطار تخزين للصور. وبما أنّه لا يتم عرض إطار الذاكرة المؤقتة مطلقًا، لا يهمّ أي جزء هو الجزء العلوي وأي جزء هو الجزء السفلي. ما يهمّ هو أنّ البكسل 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 وAlpha

لاحظتُ أنّ بعض مطوّري OpenGL يواجهون مشاكل في طريقة تعامل WebGL مع قيمة الشفافية في الذاكرة المؤقتة الخلفية (أيّ اللوحة)، لذا فكّرتُ أنّه من المفيد الاطّلاع على بعض الاختلافات بين WebGL وOpenGL في ما يتعلّق بقيمة الشفافية.

يكمن أكبر فرق بين OpenGL وWebGL في أنّ OpenGL يعرض المحتوى في مخزن خلفي لا يتم دمجه مع أي شيء، أو لا يتم دمجه بشكل فعّال مع أي شيء من خلال مدير النوافذ في نظام التشغيل، لذا لا يهمّ ما هو قيمة شفافية الصورة. يُدمج WebGL من خلال المتصفّح مع صفحة الويب، ويكون الخيار التلقائي هو استخدام شفافية مُضاعَفة مسبقًا مثل علامات <img> بتنسيق ‎ .png مع شفافية وعلامات لوحة الرسم ثنائية الأبعاد. تتوفّر في WebGL عدة طرق لجعل هذا الإجراء أشبه بـ OpenGL.

#1) أخبِر WebGL بأنّك تريد دمج العنصر مع قيمة ألفا غير مُضاعَفة مسبقًا.

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

الإعداد التلقائي هو true. بطبيعة الحال، ستظل النتيجة مركّبة على الصفحة مع أي لون خلفية ينتهي به المطاف تحت اللوحة (لون خلفية اللوحة، ولون خلفية حاوية اللوحة، ولون خلفية الصفحة، والعناصر التي تظهر خلف اللوحة إذا كانت اللوحة تحتوي على مؤشر z-index أكبر من 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);

تكون عملية محو البيانات سريعة جدًا بشكل عام لأنّ هناك حالة خاصة لها في معظم الأجهزة. لقد فعلت ذلك في معظم عروضي التوضيحية. إذا كنت ذكيًا، كنت سأستخدم الطريقة الثانية أعلاه. سأفعل ذلك بعد نشر هذه الرسالة مباشرةً. يبدو أنّ معظم مكتبات 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);

هذه هي الطرق التي أعرفها. إذا كان لديك المزيد من المعلومات، يُرجى نشرها أدناه.