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

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>

ويمكننا بعد ذلك ضبطه عند التصدير باستخدام

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

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