יסודות WebGL

Gregg Tavares
Gregg Tavares

יסודות WebGL

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

ל-WebGL יש רק 2 דברים: קואורדינטות של מרחב קליפ בדו-ממד ובצבעים. התפקיד שלכם כמתכנתים שמשתמש ב-WebGL הוא לספק את שני הדברים האלה ל-WebGL. כדי לעשות זאת, צריך לספק 2 "מצפינים". תוכנת הצללה (shader) של 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, ללא קשר לגודל הקנבס. במקרה שלמעלה אפשר לראות שאנחנו לא עושים דבר חוץ מהעברת נתוני המיקום שלנו ישירות. מכיוון שנתוני המיקום כבר נמצאים במרחב החיפוי, לא נדרשת כל פעולה מצידכם. אם אתם רוצים תלת-ממד, עליכם לספק תוכנות הצללה שממירות 3D לתלת-ממד 2D כי 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>

עכשיו אנחנו יכולים לשנות את הנתונים שלנו מ-Clipspace לפיקסלים

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

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

<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 פשוט למדי. אומנם יכול להיות קשה יותר ליצור תלת-ממד על ידי הוספת סיבוך על ידיכם, המתכנת, בצורה של תוכנות הצללה מורכבות יותר. ה-WebGL API עצמו הוא דו-ממדי ופשוט למדי.

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

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

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

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

עיבוד תמונה WebGL

עיבוד התמונה ב-WebGL מתבצע בקלות. עד כמה קל? מידע נוסף מופיע למטה.

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

לאחר מכן אנחנו מספקים משטח להצללה (shader) כדי לחפש צבעים מהמרקם.

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

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

שימוש בכמה אפקטים

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

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

כדי לעשות את זה, צריך ליצור framebuffers. ב-WebGL וב-OpenGL, Framebuffer הוא שם גרוע. WebGL/OpenGL Framebuffer הוא למעשה אוסף של מצב, ולא מאגר מכל סוג שהוא. אבל, על ידי צירוף מרקם ל-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);

עכשיו נשתמש בפונקציה הזו כדי ליצור עוד 2 מרקמים ונחבר אותם ל-2 framebuffers.

// 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 שברצונך לעבד את בד הציור במקום לאחד מה-framebers. WebGL צריך להמיר משטח קליפים בחזרה לפיקסלים. הפעולה הזו מתבצעת על סמך ההגדרות של gl.viewport. ברירת המחדל של ההגדרות של gl.viewport היא גודל הקנבס כשאנחנו מאתחלים את WebGL. מכיוון שרכיבי ה-framebup שאליהם אנחנו מבצעים רינדור הם בגודל שונה, אז אנחנו צריכים להגדיר את אזור התצוגה כראוי. לבסוף, בדוגמאות של יסודות 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-index > 0 וכו'), במילים אחרות, הצבע שה-CSS מגדיר לאזור זה של דף האינטרנט. דרך טובה לברר אם יש לך בעיות בגרסת אלפא היא להגדיר את הרקע של הקנבס לצבע בהיר כמו אדום. תוכלו לראות מיד מה קורה.

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

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

#2) לומר ל-WebGL שלא רוצים שגרסת האלפא במאגר הנתונים הזמני

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

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

מס' 3) ניקוי אלפא בסוף העיבוד

..
renderScene();
..
// Set the backbuffer's alpha to 1.0
gl.clearColor(1, 1, 1, 1);
gl.colorMask(false, false, false, true);
gl.clear(gl.COLOR_BUFFER_BIT);

הניקוי בדרך כלל מהיר מאוד כיוון שיש מקרה מיוחד לכך ברוב החומרה. עשיתי זאת ברוב ההדגמות. אם הייתי חכם, הייתי עובר לשיטה 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);

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