WebGL Fundamentals
בעזרת WebGL אפשר להציג בדפדפן גרפיקה תלת-ממדית מדהימה בזמן אמת, אבל רבים לא יודעים ש-WebGL הוא למעשה ממשק API דו-ממדי, ולא תלת-ממדי. הרשה לי להסביר.
ב-WebGL חשובים רק שני דברים. קואורדינטות של מרחב חיתוך ב-2D וצבעים. התפקיד שלכם כמתכנת שמשתמש ב-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);
אלה שני ה-shaders
<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 דו-ממדי! בדברים 2D, כנראה עדיף לעבוד בפיקסלים ולא במרחב חיתוך, אז נשנה את ה-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>
עכשיו אפשר לשנות את הנתונים ממרחב חיתוך לתמונה (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);
נשנה את הקוד שמגדיר מלבן לפונקציה כדי שנוכל להפעיל אותו למלבנים בגדלים שונים. תוך כדי כך, נאפשר גם להגדיר את הצבע. קודם כול, אנחנו מגדירים ל-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 פשוט למדי. אומנם יצירת 3D יכולה להיות מורכבת יותר, אבל המורכבות הזו מתווספת על ידכם, המתכנתים, בצורת תוכנות הצללה מורכבות יותר. ממשק ה-WebGL API עצמו הוא דו-מימדי ופשוט למדי.
מה המשמעות של type="x-shader/x-vertex" ושל type="x-shader/x-fragment"?
תגי <script>
מכילים JavaScript כברירת מחדל. אפשר לא לציין סוג או לציין type="javascript"
או type="text/javascript"
, והדפדפן יטפל בתוכן כ-JavaScript. אם תוסיפו משהו אחר, הדפדפן יתעלם מהתוכן של תג הסקריפט.
אנחנו יכולים להשתמש בתכונה הזו כדי לאחסן שיבוטים (shaders) בתגי סקריפט. יתרה מכך, אנחנו יכולים ליצור סוג משלהם ולחפש אותו ב-JavaScript כדי להחליט אם לקמפל את ה-shader כ-vertex shader או כ-fragment shader.
במקרה כזה, הפונקציה createShaderFromScriptElement
מחפשת סקריפט עם id
שצוין, ואז בודקת את type
כדי להחליט איזה סוג של שַדְר (shader) ליצור.
עיבוד תמונות ב-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;
}
לאחר מכן אנחנו מספקים שגיאת שבר (fragment 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) כדי לבצע כמה פעולות נפוצות של עיבוד תמונה. במקרה הזה נשתמש בליבה (kernel) בגודל 3x3. ליבה של עיבוד נתונים (convolution) היא פשוט מטריצה 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_
למשתני uniform, שהם ערכים שמוזנים לשיחרים, v_
למשתני varying, שהם ערכים שמועברים משיחור קודקודים לשיחור פירור ומשווים (או משתנים) בין הקודקודים לכל פיקסל שמצויר.
החלה של מספר אפקטים
השאלה הבאה שברור שצריך לשאול לגבי עיבוד תמונות היא איך מחילים כמה אפקטים?
אפשר לנסות ליצור שיבושים בזמן אמת. מספקים ממשק משתמש שמאפשר למשתמש לבחור את האפקטים שבהם הוא רוצה להשתמש, ולאחר מכן יוצרים שידרוג (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
כדי לעשות זאת, צריך ליצור מאגרי framebuffer. ב-WebGL וב-OpenGL, השם Framebuffer הוא למעשה שם לא טוב. למעשה, Framebuffer של WebGL/OpenGL הוא רק אוסף של מצבים, ולא מאגר מכל סוג שהוא. עם זאת, אם מחברים טקסטורה ל-framebuffer, אפשר לבצע עיבוד (render) בטקסטורה הזו. קודם נמיר את הקוד הישן ליצירת טקסטורה לפונקציה.
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 לדעת שאתם רוצים לבצע עיבוד (render) ב-canvas במקום באחד מ-framebuffers.
WebGL צריך לבצע המרה ממרחב חיתוך בחזרה לפיקסלים. הוא עושה זאת על סמך ההגדרות של gl.viewport
. כברירת מחדל, ההגדרות של gl.viewport
מוגדרות לגודל הלוח כשאנחנו מפעילים את WebGL. מאחר שפורמטים של framebuffers שבהם אנחנו מבצעים רינדור הם בגודל שונה מזה של הקנבס, אנחנו צריכים להגדיר את אזור התצוגה בהתאם.
לסיום, בדוגמאות הבסיסיות של WebGL הפכנו את קואורדינטת ה-Y במהלך העיבוד, כי ב-WebGL הקנבס מוצג כאשר 0,0 היא הפינה הימנית התחתונה, במקום הפינה הימנית העליונה המסורתית יותר של 2D. אין צורך בכך כשמבצעים עיבוד (רנדר) ל-framebuffer. מכיוון ש-framebuffer אף פעם לא מוצג, לא משנה איזה חלק הוא החלק העליון ואיזה חלק הוא החלק התחתון. כל מה שחשוב הוא ש-pixel 0,0 ב-framebuffer תואם ל-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 עם פרמטרים משלהם מחייבות כנראה עריכת קוד מחדש (refactoring) כדי למנוע מצב שבו הכול יהפוך למסכת פסטה גדולה. אני מקווה שהדוגמה הזו והדוגמאות הקודמות עזרו לכם להבין את WebGL קצת יותר, ושהתחלת העבודה עם 2D תעזור לכם להבין את WebGL קצת יותר בקלות. אם אמצא זמן, אנסה לכתוב עוד כמה מאמרים על יצירת גרפיקה תלת-ממדית, וגם פרטים נוספים על מה ש-WebGL עושה בפועל מתחת לפני השטח.
WebGL ואלפא
שמתי לב שלמפתחים מסוימים של OpenGL יש בעיות בטיפול של WebGL ב-alpha ב-backbuffer (כלומר, בקנבס), ולכן חשבתי שזה יהיה רעיון טוב להסביר על חלק מההבדלים בין WebGL ל-OpenGL שקשורים ל-alpha.
ההבדל הגדול ביותר בין OpenGL לבין WebGL הוא ש-OpenGL מבצע עיבוד (render) ל-backbuffer שלא משולב עם שום דבר, או שלא משולב בפועל עם שום דבר על ידי מנהל החלונות של מערכת ההפעלה, כך שלא משנה מה הערך של אלפא.
הדפדפן מבצע שילוב (composite) של WebGL עם דף האינטרנט, והברירת המחדל היא להשתמש באלפא מוכפלת מראש, כמו בתגים <img>
.png עם שקיפות ובתגי קנבס דו-ממדיים.
ב-WebGL יש כמה דרכים לעשות זאת בצורה דומה יותר ל-OpenGL.
#1) מציינים ל-WebGL שרוצים שיצרו קומפוזיציה עם אלפא ללא הכפלה מראש
gl = canvas.getContext("experimental-webgl", {premultipliedAlpha: false});
ברירת המחדל היא true. כמובן שהתוצאה עדיין תהיה מורכבת מעל הדף עם צבע הרקע שיהיה מתחת ללוח הציור (צבע הרקע של הלוח, צבע הרקע של מאגר הלוח, צבע הרקע של הדף, הדברים שמאחורי הלוח אם ללוח יש z-index > 0 וכו'). במילים אחרות, הצבע שמוגדר ב-CSS לאזור הזה בדף האינטרנט. דרך טובה לבדוק אם יש בעיות שקשורות ל-alpha היא להגדיר את הרקע של הלוח לצבע בהיר כמו אדום. תוכלו לראות מיד מה קורה.
<canvas style="background: red;"></canvas>
אפשר גם להגדיר אותו לשחור, כדי להסתיר בעיות שקשורות לאלפא.
#2) מודיעים ל-WebGL שאתם לא רוצים אלפא ב-backbuffer
gl = canvas.getContext("experimental-webgl", {alpha: false});
כך הוא יפעל יותר כמו OpenGL, כי במאגר הווידאו האחורי יהיו רק צבעים מסוג RGB. זו כנראה האפשרות הטובה ביותר, כי דפדפן טוב יכול לראות שאין לכם אלפא ולבצע אופטימיזציה של האופן שבו מתבצע השילוב של WebGL. כמובן, המשמעות היא גם שלא תהיה אלפא במאגר הווידאו האחורי, כך שאם אתם משתמשים באלפא במאגר הווידאו האחורי למטרה כלשהי, יכול להיות שהפתרון הזה לא יתאים לכם. ידוע לי על מעט אפליקציות שמשתמשות ב-alpha במאגר התמונות האחורי. לדעתי, זו הייתה צריכה להיות ברירת המחדל.
#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 צריכות להשתמש בשיטה הזו כברירת מחדל. רק המפתחים הבודדים שמשתמשים בפועל ב-alpha ליצירת קומפוזיציות של אפקטים יכולים לבקש את זה. שאר המשתמשים יקבלו את הביצועים הטובים ביותר עם כמה שפחות הפתעות.
#4) לנקות את האלפא פעם אחת ולא לבצע עיבוד (render) יותר
// 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);
כמובן, אם אתם מבצעים עיבוד תמונה ל-framebuffers משלכם, יכול להיות שתצטרכו להפעיל מחדש את העיבוד ל-alpha ואז לכבות אותו שוב כשתעברו לעיבוד תמונה ללוח הציור.
#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);
אלה השיטות שידועות לי. אם יש לך עוד הצעות, אפשר לפרסם אותן בהמשך.