WebGL אורתוגרפיה בתלת-ממד

Gregg Tavares
Gregg Tavares

WebGL אורתוגרפי 3D

פוסט זה הוא המשך של סדרת פוסטים בנושא WebGL. הקבוצה הראשונה התחילה עם יסודות וקודמת לה הייתה עוסקת במטריצות דו-ממדיות על מטריצות דו-ממדיות. אם לא קראת את השמות, כדאי לך לעיין בהם קודם. בפוסט האחרון הסברנו על האופן שבו עובדות מטריצות דו-ממדיות. דיברנו על תרגום, סיבוב, התאמה לעומס (scaling) ואפילו הקרנת תוכן מפיקסלים לשטח הקליפ אפשר לבצע באמצעות מטריצה אחת ומתמטית מטריצת קסם. יצירת תלת-ממד היא רק צעד קטן. בדוגמאות הדו-ממדיות הקודמות שלנו היו נקודות דו-ממדיות (x, y) שהכפלנו במטריצה של 3x3. כדי ליצור תלת-ממד, אנחנו זקוקים לנקודות תלת-ממדיות (x, y, z) ומטריצה של 4x4. ניקח את הדוגמה האחרונה ונשנה אותה לתלת-ממד. נשתמש שוב ב-F אבל הפעם באות F בתלת-ממד. הדבר הראשון שאנחנו צריכים לעשות הוא לשנות את תוכנת ההצללה בקוד הקוד כך שתטפל בתלת-ממד. זו תוכנת ההצללה הישנה.

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

uniform mat3 u_matrix;

void main() {
// Multiply the position by the matrix.
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
</script>

והנה התמונה החדשה

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

uniform mat4 u_matrix;

void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;
}
</script>

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

...

gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);

...

// Fill the buffer with the values that define a letter 'F'.
function setGeometry(gl) {
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([
        // left column
        0,   0,  0,
        30,   0,  0,
        0, 150,  0,
        0, 150,  0,
        30,   0,  0,
        30, 150,  0,

        // top rung
        30,   0,  0,
        100,   0,  0,
        30,  30,  0,
        30,  30,  0,
        100,   0,  0,
        100,  30,  0,

        // middle rung
        30,  60,  0,
        67,  60,  0,
        30,  90,  0,
        30,  90,  0,
        67,  60,  0,
        67,  90,  0]),
    gl.STATIC_DRAW);
}

בשלב הבא, עלינו לשנות את כל הפונקציות של המטריצה מדו-ממד לתלת-ממד אלו הן הגרסאות הדו-ממדיות (לפני) של MakeTranslation, MakeRotation ו-MakeScale

function makeTranslation(tx, ty) {
return [
1, 0, 0,
0, 1, 0,
tx, ty, 1
];
}

function makeRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c,-s, 0,
s, c, 0,
0, 0, 1
];
}

function makeScale(sx, sy) {
return [
sx, 0, 0,
0, sy, 0,
0, 0, 1
];
}

והנה גרסאות התלת-ממד המעודכנות.

function makeTranslation(tx, ty, tz) {
return [
    1,  0,  0,  0,
    0,  1,  0,  0,
    0,  0,  1,  0,
    tx, ty, tz, 1
];
}

function makeXRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);

return [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
];
};

function makeYRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);

return [
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
];
};

function makeZRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
    c, s, 0, 0,
-s, c, 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1,
];
}

function makeScale(sx, sy, sz) {
return [
sx, 0,  0,  0,
0, sy,  0,  0,
0,  0, sz,  0,
0,  0,  0,  1,
];
}

שימו לב שעכשיו יש לנו 3 פונקציות של סיבובים. היינו צריכים רק שקף דו-ממדי, כי הסתובבנו ביעילות רק מסביב לציר ה-Z. אבל עכשיו, כדי לעשות תלת-ממד, אנחנו רוצים גם להיות מסוגלים להסתובב סביב ציר ה-X וציר ה-Y. אחרי שמסתכלים על כולם, אפשר לראות שהם דומים מאוד. אם היינו עובדים עליהם, הייתם רואים שהם פשוטים יותר כמו קודם

סיבוב Z

newX = x * c + y * s;
newY = x * -s + y * c;

סיבוב Y


newX = x * c + z * s;
newZ = x * -s + z * c;

סיבוב X

newY = y * c + z * s;
newZ = y * -s + z * c;

בנוסף, צריך לעדכן את פונקציית ההקרנה. הנה התמונה הישנה

function make2DProjection(width, height) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
];
}

שהומרו מפיקסלים לשטח קליפ. בניסיון הראשון שלנו להרחיב אותו לתלת-ממד, בואו ננסה

function make2DProjection(width, height, depth) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
    2 / width, 0, 0, 0,
    0, -2 / height, 0, 0,
    0, 0, 2 / depth, 0,
-1, 1, 0, 1,
];
}

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

// Compute the matrices
var projectionMatrix =
    make2DProjection(canvas.width, canvas.height, canvas.width);
var translationMatrix =
    makeTranslation(translation[0], translation[1], translation[2]);
var rotationXMatrix = makeXRotation(rotation[0]);
var rotationYMatrix = makeYRotation(rotation[1]);
var rotationZMatrix = makeZRotation(rotation[2]);
var scaleMatrix = makeScale(scale[0], scale[1], scale[2]);

// Multiply the matrices.
var matrix = matrixMultiply(scaleMatrix, rotationZMatrix);
matrix = matrixMultiply(matrix, rotationYMatrix);
matrix = matrixMultiply(matrix, rotationXMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
matrix = matrixMultiply(matrix, projectionMatrix);

// Set the matrix.
gl.uniformMatrix4fv(matrixLocation, false, matrix);

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

// Draw the geometry.
gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);

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

<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;
attribute vec4 a_color;

uniform mat4 u_matrix;

varying vec4 v_color;

void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;

// Pass the color to the fragment shader.
v_color = a_color;
}
</script>

ואנחנו צריכים להשתמש בצבע הזה בתוכנת ההצללה בקטעים

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

// Passed in from the vertex shader.
varying vec4 v_color;

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

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

...
var colorLocation = gl.getAttribLocation(program, "a_color");

...
// Create a buffer for colors.
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(colorLocation);

// We'll supply RGB as bytes.
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);

// Set Colors.
setColors(gl);

...
// Fill the buffer with colors for the 'F'.

function setColors(gl) {
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Uint8Array([
        // left column front
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,

        // top rung front
    200,  70, 120,
    200,  70, 120,
    ...
    ...
    gl.STATIC_DRAW);
}

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

משולש מתעקל.

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

gl.enable(gl.CULL_FACE);

שאנחנו עושים רק פעם אחת, בתחילת התוכנית. כשהתכונה הזו פועלת, WebGL בוחר כברירת מחדל "קיפול" של משולשים שפונים לאחור. "Culling" במקרה הזה היא מילה מתוחכמת ל "לא ציור". שים לב: בכל הנוגע ל-WebGL, ההחלטה אם משולש נע בכיוון השעון או נגדו בכיוון השעון תלוי בקודקודים של המשולש במרחב הצילום. במילים אחרות, WebGL מוצא אם המשולש הוא מלפנים או אחורה אחרי החלת המתמטיקה על הקודקודים בכלי ההצללה בקוד. המשמעות היא, לדוגמה, משולש בכיוון השעון שקנה המידה שלו הוא 1-X הופך למשולש נגד כיוון השעון, או משולש שסובב ב-180 מעלות סביב ציר ה-X או ה-Y הופך למשולש נגד כיוון השעון. מכיוון שהשבתנו את CULL_FACE, נוכל לראות גם משולש בכיוון השעון(קדמי) וגם משולש נגד כיוון השעון(אחורה). עכשיו, לאחר שהפעלנו אותו, בכל פעם שמשולש קדמי מתהפך בגלל שינוי קנה מידה או סיבוב או מסיבה כלשהי, WebGL לא יצייר אותו. זה דבר טוב, משום שכאשר אתם מסתובבים בתלת-ממד, אתם בדרך כלל רוצים אילו משולשים יפנו אליכם וייחשבו כלפי חוץ.

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

1,   2,   3,
40,  50,  60,
700, 800, 900,

אנחנו פשוט הופכים את שני הקודקודים האחרונים כדי להתקדם.

1,   2,   3,
700, 800, 900,
40,  50,  60,

זה קרוב יותר, אבל עדיין יש בעיה אחת נוספת. גם אם כל המשולשים פונים לכיוון הנכון וכשהמשולשים פונים מאחור, עדיין יש מקומות שבהם משולשים שאמורים להיות מאחור משורטטים מעל משולשים שאמורים להיות מלפנים. מזינים את DEPTH BUFFER. חוצץ עומק, שנקרא לפעמים Z-Buffer, הוא מלבן של depth פיקסלים, פיקסל עומק אחד לכל פיקסל צבע המשמש ליצירת התמונה. מכיוון ש-WebGL מצייר כל פיקסל של צבע, הוא יכול גם לצייר פיקסל עומק. הפעולה הזו מתבצעת על סמך הערכים שהחזרנו מההצללה (shader) הקודקוד של Z. בדיוק כמו שהיינו צריכים להמיר לשטח קליפ עבור X ו-Y, כך גם Z נמצא בשטח קליפ או (1- עד 1). ערך זה מומר לערך של רווח עומק (0 עד 1+). לפני ש-WebGL מצייר פיקסל צבעוני, הוא יבדוק את פיקסל העומק המתאים. אם ערך העומק של הפיקסל עומד לצייר גדול מהערך של פיקסל העומק התואם, WebGL לא מצייר את פיקסל הצבע החדש. אחרת, הוא מצייר את פיקסל הצבע החדש גם עם הצבע של תוכנת ההצללה בקטעים, וגם מצייר את פיקסל העומק עם ערך העומק החדש. כלומר, פיקסלים שנמצאים מאחורי פיקסלים אחרים לא יצוירו. אנחנו יכולים להפעיל את התכונה הזו באותה מידה שבה אנחנו מפעילים את האפשרות

gl.enable(gl.DEPTH_TEST);

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

// Draw the scene.
function drawScene() {
// Clear the canvas AND the depth buffer.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
...

בפוסט הבא אדבר על האופן שבו יש לתת לו פרספקטיבה.

למה המאפיין vec4 אבל gl.vertexAttribPointer גודל 3

אם אתם מתעניינים בפרטים, אולי שמתם לב שהגדרנו את שני המאפיינים שלנו בתור

attribute vec4 a_position;
attribute vec4 a_color;

ושניהם שייכים ל-vec4, אבל כשאנחנו אומרים ל-WebGL איך להוציא נתונים ממאגרי האחסון הייעודיים שלנו

gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);

הערך '3' בכל אחד מהם מציין שפשוט צריך לשלוף 3 ערכים לכל מאפיין. זה עובד כי תוכנת ההצללה (shader) של קודקוד ה-WebGL מספקת ברירות מחדל לאלה שלא אתם מספקים. ערכי ברירת המחדל הם 0, 0, 0, 1 כאשר x = 0, y = 0, z = 0 ו-w = 1. זו הסיבה לכך שהצלחנו לספק את ההצללה הדו-ממדית הקודמת באופן מפורש. העברנו את הערכים x ו-y, והיינו צריכים את המספר 1 במקום z, אבל מכיוון שברירת המחדל של z היא 0, נאלצנו לציין את 1 באופן מפורש. לתלת-ממד, למרות שאנחנו לא מספקים את הערך 'w', ערך ברירת המחדל הוא 1, וזה מה שאנחנו צריכים כדי שהמתמטיקה של המטריצה תפעל.