أساسيات WebGL

أساسيات WebGL

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

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

في ما يلي عنصرَي التظليل

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

ماذا يعني type="x-shader/x-vertex" and type="x-shader/x-segment"؟

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

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

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

معالجة صور WebGL

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

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

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

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

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