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

Gregg Tavares
Gregg Tavares

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

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

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

בשלב הבא צריך לשנות את כל הפונקציות של המטריצה בדו-ממד לתלת-ממד הנה גרסאות הדו-ממדיות (לפני) של דוגמאות לתרגום, 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 יחידות. אני אעביר ערך שדומה ל-width בשביל העומק כך השטח שלנו יהיה ברוחב של 0 עד 0 עד 1 פיקסלים, וגובה של 0 עד 0 פיקסלים, לגבי העומק, יהיה: 'עומק' / 2 עד '+עומק' / 2. בסוף אנחנו צריכים לעדכן את הקוד שמחשב את המטריצה.

// 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 2 משולשים לכל מלבן x 3 קודקודים למשולש הוא 96 קודקודים. אם אתם רוצים לראות את כולם, הציגו את המקור בדוגמה. אנחנו צריכים לשרטט עוד קודקודים

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

קשה מספיק להזיז את פסי ההזזה כדי לזהות שזה תלת-ממד. רוצה לנסות? צובעים כל מלבן בצבע שונה. כדי לעשות את זה, נוסיף עוד לרכיב ההצללה של הקודקוד שלנו ומשתנה כדי להעביר אותו מהקודקוד ל תוכנת ההצללה (shader) של המקטעים. הנה הכלי החדש להצללה (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" ומשולשים מאחור. 'Culling' במקרה הזה היא מילה מפוארת ל-"not art" (לא ציור). שימו לב שכל עוד יש קשר ל-WebGL, לא משנה אם מדובר במשולש נחשב כ מפורט בכיוון השעון או נגד כיוון השעון תלוי הקודקודים של המשולש בחיתוך. במילים אחרות, WebGL האם משולש מסוים לפני או אחורה אחרי החלת המתמטיקה על בקודקודים בצללית של קודקוד. זה אומר, למשל, בכיוון השעון משולש שגודלו ב-X על 1-הופך למשולש נגד כיוון השעון, או משולש בכיוון השעון שמסובב ב-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,

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

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

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 ערכים לכל מאפיין. זה עובד כי ב-Sshader הקודקוד, WebGL מספק ברירות מחדל בשביל שאתם לא מספקים. הגדרות ברירת המחדל הן 0, 0, 0, 1 כאשר x = 0, y = 0, z = 0 ו-w = 1. לכן, בכלי הישן להצללה של קודקוד דו-ממדי, נאלצנו מספקים את הערך 1. עברנו ב-x ו-y והיינו צריכים 1 עבור z כי ברירת המחדל של z היא 0 היינו צריכים לציין במפורש את 1. ל-3D למרות שאנחנו לא מספקים ברירת המחדל היא 1, אנחנו צריכים כדי שהמטריצה המתמטית תפעל.