أساسيات WebGL

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

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

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

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

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

هناك بعض الأمور التي يجب أن أراجعها.

Calling gl.bindFramebuffer with null tells WebGL you want to render to the canvas instead of to one of your framebuffers. يجب أن تحوّل 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 والإصدار الأولي

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

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

#1. إبلاغ WebGL بأنّك تريد تركيبه باستخدام ألفا غير مضروبة مسبقًا

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

وتكون القيمة التلقائية مضبوطة على true. Of course the result will still be composited over page with whatever background color ends up being under the canvas (the canvas's background color, the canvas's container background color, the page's background color, the stuff behind the canvas if the canvas has a z-index > 0, etc…) in other words, the color CSS defines for that area of the webpage. من الطرق الجيدة لمعرفة ما إذا كانت هناك أي مشاكل في قناة ألفا هي ضبط خلفية لوحة العرض على لون ساطع مثل الأحمر. وستظهر لك على الفور الأحداث الجارية.

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

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