היסודות של WebGL

Gregg Tavares
Gregg Tavares

‫WebGL מאפשר להציג גרפיקה תלת-ממדית מדהימה בזמן אמת בדפדפן, אבל מה שהרבה אנשים לא יודעים הוא ש-WebGL הוא למעשה API דו-ממדי, ולא API תלת-ממדי. הרשה לי להסביר.

ל-WebGL יש רק שתי מטרות. קואורדינטות של מרחב החיתוך בדו-ממד וצבעים. התפקיד שלכם כמתכנתים שמשתמשים ב-WebGL הוא לספק ל-WebGL את שני הדברים האלה. כדי לעשות את זה, צריך לספק 2 "הצללות". ‫Vertex shader שמספק את הקואורדינטות של מרחב החיתוך ו-fragment shader שמספק את הצבע. הקואורדינטות של מרחב החיתוך תמיד נעות בין ‎-1 ל-‎+1, לא משנה מה גודל הקנבס. הנה דוגמה פשוטה ל-WebGL שמראה את WebGL בצורה הכי פשוטה.

// Get A WebGL context
var canvas = document.getElementById("canvas");
var gl = canvas.getContext("experimental-webgl");

// setup a GLSL program
var vertexShader = createShaderFromScriptElement(gl, "2d-vertex-shader");
var fragmentShader = createShaderFromScriptElement(gl, "2d-fragment-shader");
var program = createProgram(gl, [vertexShader, fragmentShader]);
gl.useProgram(program);

// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");

// Create a buffer and put a single clipspace rectangle in
// it (2 triangles)
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([
        -1.0, -1.0,
         1.0, -1.0,
        -1.0,  1.0,
        -1.0,  1.0,
         1.0, -1.0,
         1.0,  1.0]),
    gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

// draw
gl.drawArrays(gl.TRIANGLES, 0, 6);

הנה 2 ההצללות

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

void main() {
  gl_Position = vec4(a_position, 0, 1);
}
</script>

<script id="2d-fragment-shader" type="x-shader/x-fragment">
void main() {
  gl_FragColor = vec4(0,1,0,1);  // green
}
</script>

שוב, הקואורדינטות של מרחב החיתוך תמיד נעות בין ‎-1 ל-‎+1, בלי קשר לגודל של אזור הציור. בדוגמה שלמעלה, אנחנו רק מעבירים את נתוני המיקום שלנו ישירות. מכיוון שנתוני המיקום כבר נמצאים במרחב החיתוך, אין מה לעשות. אם אתם רוצים תלת-ממד, אתם צריכים לספק הצללות שממירות מתלת-ממד לדו-ממד כי WebGL הוא API דו-ממדי! עבור דברים דו-ממדיים, כנראה תעדיפו לעבוד בפיקסלים ולא במרחב החיתוך, אז בואו נשנה את ה-shader כדי שנוכל לספק מלבנים בפיקסלים ולגרום לו להמיר אותם למרחב החיתוך בשבילנו. זהו ה-vertex shader החדש

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform vec2 u_resolution;

void main() {
   // convert the rectangle from pixels to 0.0 to 1.0
   vec2 zeroToOne = a_position / u_resolution;

   // convert from 0->1 to 0->2
   vec2 zeroToTwo = zeroToOne * 2.0;

   // convert from 0->2 to -1->+1 (clipspace)
   vec2 clipSpace = zeroToTwo - 1.0;

   gl_Position = vec4(clipSpace, 0, 1);
}
</script>

עכשיו אנחנו יכולים לשנות את הנתונים שלנו ממרחב החיתוך לפיקסלים

// set the resolution
var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);

// setup a rectangle from 10,20 to 80,30 in pixels
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
    10, 20,
    80, 20,
    10, 30,
    10, 30,
    80, 20,
    80, 30]), gl.STATIC_DRAW);

יכול להיות שהמלבן יהיה קרוב לתחתית האזור הזה. ב-WebGL, הפינה השמאלית התחתונה היא 0,0. כדי להפוך אותה לפינה השמאלית העליונה המסורתית יותר שמשמשת לממשקי API של גרפיקה דו-ממדית, אנחנו פשוט הופכים את קואורדינטת ה-Y.

gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

בואו נהפוך את הקוד שמגדיר מלבן לפונקציה, כדי שנוכל לקרוא לה למלבנים בגדלים שונים. בזמן שאנחנו עושים את זה, נגדיר את הצבע. קודם כל, נגדיר ש-fragment shader יקבל קלט אחיד של צבע.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

uniform vec4 u_color;

void main() {
   gl_FragColor = u_color;
}
</script>

והנה הקוד החדש שמצייר 50 מלבנים במקומות אקראיים ובצבעים אקראיים.

...

  var colorLocation = gl.getUniformLocation(program, "u_color");
  ...
  // Create a buffer
  var buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.enableVertexAttribArray(positionLocation);
  gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

  // draw 50 random rectangles in random colors
  for (var ii = 0; ii < 50; ++ii) {
    // Setup a random rectangle
    setRectangle(
        gl, randomInt(300), randomInt(300), randomInt(300), randomInt(300));

    // Set a random color.
    gl.uniform4f(colorLocation, Math.random(), Math.random(), Math.random(), 1);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  }
}

// Returns a random integer from 0 to range - 1.
function randomInt(range) {
  return Math.floor(Math.random() * range);
}

// Fills the buffer with the values that define a rectangle.
function setRectangle(gl, x, y, width, height) {
  var x1 = x;
  var x2 = x + width;
  var y1 = y;
  var y2 = y + height;
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
     x1, y1,
     x2, y1,
     x1, y2,
     x1, y2,
     x2, y1,
     x2, y2]), gl.STATIC_DRAW);
}

אני מקווה שאתם רואים ש-WebGL הוא למעשה API די פשוט. אמנם תלת-ממד יכול להיות מורכב יותר, אבל המורכבות הזו מתווספת על ידכם, המתכנתים, בצורה של הצללות מורכבות יותר. ה-API של WebGL הוא דו-ממדי ודי פשוט.

מה המשמעות של type="x-shader/x-vertex" ו-type="x-shader/x-fragment"?

תגי <script> כוללים JavaScript כברירת מחדל. אפשר לא להזין סוג או להזין type="javascript" או type="text/javascript" והדפדפן יפרש את התוכן כ-JavaScript. אם תזינו ערך אחר, הדפדפן יתעלם מהתוכן של תג הסקריפט.

אנחנו יכולים להשתמש בתכונה הזו כדי לאחסן שיידרים בתגי סקריפט. אפילו יותר טוב, אנחנו יכולים להמציא סוג משלנו וב-JavaScript שלנו לחפש אותו כדי להחליט אם לקמפל את ה-shader כ-vertex shader או כ-fragment shader.

במקרה הזה, הפונקציה createShaderFromScriptElement מחפשת סקריפט עם id שצוין, ואז בודקת את type כדי להחליט איזה סוג של הצללה ליצור.

עיבוד תמונות ב-WebGL

קל לעבד תמונות ב-WebGL. עד כמה זה קל? ההסבר מופיע בהמשך.

כדי לצייר תמונות ב-WebGL, צריך להשתמש בטקסטורות. בדומה לאופן שבו WebGL מצפה לקואורדינטות של מרחב החיתוך בזמן רינדור במקום פיקסלים, WebGL מצפה לקואורדינטות של טקסטורה בזמן קריאת טקסטורה. הקואורדינטות של המרקם הן מ-0.0 עד 1.0, בלי קשר לממדים של המרקם. מכיוון שאנחנו מציירים רק מלבן אחד (למעשה, 2 משולשים), אנחנו צריכים להגיד ל-WebGL לאיזה מקום בטקסטורה כל נקודה במלבן מתאימה. נעביר את המידע הזה מה-vertex shader ל-fragment shader באמצעות סוג מיוחד של משתנה שנקרא varying. הוא נקרא משתנה כי הוא משתנה. ‫WebGL יבצע אינטרפולציה של הערכים שנספק ב-vertex shader כשהוא יצייר כל פיקסל באמצעות ה-fragment shader. נשתמש ב-vertex shader מסוף החלק הקודם, ונוסיף מאפיין כדי להעביר את קואורדינטות הטקסטורה, ואז נעביר אותן ל-fragment 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);
...

עכשיו, כשאנחנו יודעים איך להפנות לפיקסלים אחרים, נשתמש בגרעין קונבולוציה כדי לבצע עיבוד תמונה נפוץ. במקרה הזה נשתמש בקרנל בגודל 3x3. גרעין קונבולוציה הוא פשוט מטריצה בגודל 3x3 שבה כל ערך במטריצה מייצג את הערך שבו צריך להכפיל את 8 הפיקסלים שמסביב לפיקסל שאנחנו מעבדים. לאחר מכן אנחנו מחלקים את התוצאה במשקל של הליבה (הסכום של כל הערכים בליבה) או ב-1.0, הגדול מביניהם. כאן יש מאמר די טוב בנושא. והנה מאמר נוסף שמציג קוד אמיתי אם הייתם כותבים את זה ידנית ב-C++‎. במקרה שלנו, אנחנו הולכים לעשות את העבודה הזו ב-shader, אז הנה ה-fragment shader החדש.

<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_ למשתנים אחידים (uniforms) שהם קלט להצללות, v_ למשתנים משתנים (varyings) שהם ערכים שמועברים מהצללת קודקודים להצללת פרגמנטים, ומשתנים (או עוברים אינטרפולציה) בין הקודקודים לכל פיקסל שמצויר.

הוספת כמה אפקטים

השאלה הבאה שמתבקשת לגבי עיבוד תמונות היא איך אפשר להחיל כמה אפקטים?

אפשר לנסות ליצור הצללות תוך כדי תנועה. צריך לספק ממשק משתמש שמאפשר למשתמש לבחור את האפקטים שהוא רוצה להשתמש בהם, ואז ליצור הצללה שמבצעת את כל האפקטים. יכול להיות שזה לא תמיד אפשרי, אבל הטכניקה הזו משמשת לעיתים קרובות ליצירת אפקטים של גרפיקה בזמן אמת. דרך גמישה יותר היא להשתמש ב-2 טקסטורות נוספות ולעבד כל טקסטורה בתורה, לעבור ביניהן ולהחיל את האפקט הבא בכל פעם.

Original Image -> [Blur]        -> Texture 1
Texture 1      -> [Sharpen]     -> Texture 2
Texture 2      -> [Edge Detect] -> Texture 1
Texture 1      -> [Blur]        -> Texture 2
Texture 2      -> [Normal]      -> Canvas

כדי לעשות את זה, אנחנו צריכים ליצור מקטעי Frame buffer. ב-WebGL וב-OpenGL, השם Framebuffer הוא למעשה שם לא מוצלח. ‫WebGL/OpenGL Framebuffer הוא למעשה רק אוסף של מצבים, ולא באמת מאגר נתונים זמני (buffer) מכל סוג שהוא. אבל אם נצרף טקסטורה ל-Frame buffer, נוכל לרנדר לתוך הטקסטורה הזו. קודם כל, נהפוך את הקוד הישן ליצירת טקסטורה לפונקציה

function createAndSetupTexture(gl) {
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Set up texture so we can render any size image and so we are
  // working with pixels.
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

  return texture;
}

// Create a texture and put the image in it.
var originalImageTexture = createAndSetupTexture(gl);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

עכשיו נשתמש בפונקציה הזו כדי ליצור עוד 2 טקסטורות ולצרף אותן ל-2 מקטעי Frame buffer.

// 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 שאתם רוצים לעבד את ה-canvas במקום את אחד ממקטעי ה-Frame buffer. ‫WebGL צריך להמיר ממרחב החיתוך בחזרה לפיקסלים. הוא עושה את זה על סמך ההגדרות של gl.viewport. ההגדרות של gl.viewport מוגדרות כברירת מחדל לגודל של אזור היצירה כשאנחנו מאתחלים את WebGL. מכיוון שה-framebuffers שאנחנו מעבדים הם בגודל שונה מהקנבס, אנחנו צריכים להגדיר את אזור התצוגה בהתאם. לבסוף, בדוגמאות של יסודות WebGL הפכנו את קואורדינטת ה-Y בזמן הרינדור, כי WebGL מציג את ה-canvas כך שהפינה השמאלית התחתונה היא 0,0, במקום הפינה השמאלית העליונה, שהיא מסורתית יותר לדו-ממד. אין צורך בכך כשמעבדים ל-Frame buffer. מכיוון שה-Frame buffer אף פעם לא מוצג, לא משנה איזה חלק הוא העליון ואיזה הוא התחתון. הדבר היחיד שחשוב הוא שפיקסל 0,0 במקטע Frame buffer תואם ל-0,0 בחישובים שלנו. כדי לפתור את הבעיה, הוספתי עוד קלט ל-shader שמאפשר להגדיר אם להפוך את התמונה או לא.

<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 מבצע רינדור למאגר נתונים זמני (backbuffer) שלא משולב עם שום דבר, או למעשה לא משולב עם שום דבר על ידי מנהל החלונות של מערכת ההפעלה, כך שערך האלפא לא משנה. WebGL משולב על ידי הדפדפן עם דף האינטרנט, וברירת המחדל היא להשתמש באלפא מוכפל מראש, בדומה לתגי <img> של .png עם שקיפות ותגי קנבס דו-ממדיים. ב-WebGL יש כמה דרכים להפוך את זה ליותר דומה ל-OpenGL.

1. הגדרת WebGL כך שיורכב עם אלפא ללא הכפלה מראש

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

ערך ברירת המחדל הוא true. כמובן שהתוצאה עדיין תהיה מורכבת מעל הדף עם צבע הרקע שיהיה מתחת ל-canvas (צבע הרקע של ה-canvas, צבע הרקע של מאגר ה-canvas, צבע הרקע של הדף, הדברים שמאחורי ה-canvas אם ל-canvas יש z-index > 0 וכו'…) במילים אחרות, הצבע שמוגדר ב-CSS לאזור הזה של דף האינטרנט. דרך טובה לבדוק אם יש בעיות בערוץ האלפא היא להגדיר את הרקע של אזור העריכה לצבע בהיר כמו אדום. אתם תראו מיד מה קורה.

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

אפשר גם להגדיר אותו לשחור, וכך להסתיר את כל הבעיות שקשורות לערוץ האלפא.

2. מגדירים ב-WebGL שלא רוצים אלפא ב-backbuffer

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

כך הוא יפעל יותר כמו OpenGL, כי למאגר הנתונים הזמני יהיה רק RGB. זו כנראה האפשרות הכי טובה, כי דפדפן טוב יכול לזהות שאין לכם אלפא ולבצע אופטימיזציה של אופן ההרכבה של WebGL. כמובן שזה גם אומר שלא יהיה אלפא במאגר האחורי, אז אם אתם משתמשים באלפא במאגר האחורי למטרה כלשהי, יכול להיות שזה לא יעבוד. יש מעט אפליקציות שאני מכיר שמשתמשות באלפא במאגר הנתונים הזמני (backbuffer). לדעתי, זו הייתה צריכה להיות ברירת המחדל.

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

כמובן שאם אתם מבצעים רינדור למאגרי פריימים משלכם, יכול להיות שתצטרכו להפעיל שוב את הרינדור לאלפא ואז להשבית אותו שוב כשתעברו לרינדור ל-canvas.

יתרון 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);

אלה השיטות שאני מכיר. אם אתם מכירים עוד, אתם מוזמנים לפרסם אותם למטה.