WebGL سه‌بعدی قائم‌سنجی

Gregg Tavares
Gregg Tavares

WebGL سه‌بعدی قائم‌سنجی

این پست ادامه مجموعه‌ای از پست‌ها درباره WebGL است. اولین ویدیو با اصول اولیه شروع شد و ویدیو قبلی درباره ماتریس‌های دوبعدی بود درباره ماتریس‌های دوبعدی. اگر آن‌ها را نخوانده‌اید، لطفاً ابتدا آن‌ها را مشاهده کنید. در پست قبلی، نحوه کار ماتریس‌های دوبعدی را بررسی کردیم. درباره انتقال، چرخش، مقیاس‌بندی، و حتی طرح‌ریزی از پیکسل‌ها به فضای برش صحبت کردیم که همه این‌ها را می‌توان با ۱ ماتریس و کمی جادوی ریاضی ماتریس انجام داد. از آنجا تا سه‌بعدی کردن فقط یک گام کوچک فاصله است. در مثال‌های دوبعدی قبلی، نقاط دوبعدی (x, y) داشتیم که آن‌ها را در یک ماتریس ۳×۳ ضرب کردیم. برای انجام سه‌بعدی، به نقاط سه‌بعدی (x، y، z) و یک ماتریس ۴×۴ نیاز داریم. بیایید آخرین مثالمان را درنظر بگیریم و آن را به سه‌بعدی تغییر دهیم. دوباره از 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,
];
}

توجه کنید که اکنون ۳ تابع چرخش داریم. ما فقط به یک مورد در فضای دوبعدی نیاز داشتیم، زیرا درواقع فقط حول محور 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 را برای عمق وارد می‌کنم بنابراین فضای ما از ۰ تا عرض پیکسل پهنا، و از ۰ تا ارتفاع پیکسل بلندی خواهد داشت، اما برای عمق، از عمق / ۲- تا عمق / ۲+ خواهد بود. درنهایت باید کدی را که ماتریس را محاسبه می‌کند به‌روز کنیم.

// 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 کنونی ما از ۳ مستطیل ساخته شده است که هرکدام ۲ مثلث دارند. برای سه‌بعدی کردن آن درمجموع به ۱۶ مستطیل نیاز است. تعدادشان زیاد است و نمی‌توان همه را در اینجا فهرست کرد. ‫۱۶ مستطیل × ۲ مثلث در هر مستطیل × ۳ رأس در هر مثلث برابر با ۹۶ رأس است. اگر می‌خواهید همه آن‌ها را ببینید، منبع را در نمونه مشاهده کنید. باید رأس‌های بیشتری بکشیم، بنابراین

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

با حرکت دادن لغزنده‌ها، تشخیص سه‌بعدی بودن آن بسیار دشوار است. بیایید امتحان کنیم هر مستطیل را با رنگ متفاوتی رنگ‌آمیزی کنیم. برای انجام این کار، مشخصه دیگری به سایه‌زن رأس خود اضافه می‌کنیم و یک متغیر برای انتقال آن از سایه‌زن رأس به سایه‌زن قطعه اضافه می‌کنیم. این سایه‌زن رأس جدید است

<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 به‌طور پیش‌فرض مثلث‌های پشت‌نما را «حذف» می‌کند. «حذف» در این مورد کلمه‌ای فانتزی برای «نکشیدن» است. توجه داشته باشید که تا جایی که به WebGL مربوط می‌شود، اینکه مثلثی ساعت‌گرد یا پادساعت‌گرد درنظر گرفته شود به رئوس آن مثلث در فضای برش بستگی دارد. به عبارت دیگر، WebGL تشخیص می‌دهد که آیا یک مثلث جلو یا عقب است، پس‌از اینکه شما ریاضیات را به رئوس در سایه‌زن رأس اعمال کرده‌اید. یعنی مثلاً یک مثلث ساعت‌گرد که در محور X با ضریب ۱- مقیاس‌بندی شده است به مثلث پادساعت‌گرد تبدیل می‌شود یا یک مثلث ساعت‌گرد که ۱۸۰ درجه حول محور X یا Y چرخانده شده است به مثلث پادساعت‌گرد تبدیل می‌شود. چون CULL_FACE را غیرفعال کرده بودیم، می‌توانیم هر دو مثلث ساعت‌گرد(جلو) و پادساعت‌گرد(پشت) را ببینیم. اکنون که آن را روشن کرده‌ایم، هروقت مثلثی که رو به جلو است به‌دلیل مقیاس‌بندی یا چرخش یا هر دلیل دیگری برگردد، WebGL آن را ترسیم نخواهد کرد. این چیز خوبی است زیرا وقتی چیزی را در حالت سه‌بعدی می‌چرخانید، معمولاً می‌خواهید هر مثلثی که روبه‌روی شما قرار دارد به‌عنوان مثلث روبه‌جلو درنظر گرفته شود.

سلام! همه مثلث‌ها کجا رفتند؟ معلوم می‌شود که بسیاری از آن‌ها در جهت اشتباه قرار دارند. آن را بچرخانید و وقتی به طرف دیگر نگاه کنید، آن‌ها را خواهید دید. خوشبختانه رفع آن آسان است. ما فقط به این نگاه می‌کنیم که کدام یک از آن‌ها برعکس هستند و ۲ تا از رأس‌های آن‌ها را عوض می‌کنیم. برای مثال، اگر یک مثلث رو به عقب

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

فقط ۲ رأس آخر را برمی‌گردانیم تا رو به جلو شود.

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

این نزدیک‌تر است اما هنوز یک مشکل دیگر وجود دارد. حتی با وجود اینکه همه مثلث‌ها در جهت درست قرار دارند و مثلث‌های پشت حذف شده‌اند، هنوز جاهایی وجود دارد که مثلث‌هایی که باید در پشت باشند، روی مثلث‌هایی که باید در جلو باشند کشیده می‌شوند. DEPTH BUFFER را وارد کنید. میان‌گیر عمق، که گاهی میان‌گیر Z نامیده می‌شود، مستطیلی از depth پیکسل است، یک پیکسل عمق برای هر پیکسل رنگی که برای ساختن تصویر استفاده می‌شود. همان‌طور که WebGL هر پیکسل رنگی را ترسیم می‌کند، می‌تواند پیکسل عمق را نیز ترسیم کند. این کار را براساس مقادیری که از سایه‌زن رأس برای Z برمی‌گردانیم انجام می‌دهد. درست مثل اینکه باید به فضای برش برای X و Y تبدیل می‌کردیم، بنابراین Z نیز در فضای برش یا (-1 تا +1) است. سپس این مقدار به مقدار فضای عمق (۰ تا ۱+) تبدیل می‌شود. قبل‌از اینکه WebGL پیکسل رنگی را ترسیم کند، پیکسل عمق مربوطه را بررسی می‌کند. اگر مقدار عمق پیکسلی که قرار است ترسیم شود بیشتر از مقدار پیکسل عمق متناظر باشد، WebGL پیکسل رنگ جدید را ترسیم نمی‌کند. درغیراین‌صورت، هم پیکسل رنگ جدید را با رنگ سایه‌زن قطعه شما ترسیم می‌کند و هم پیکسل عمق را با مقدار عمق جدید ترسیم می‌کند. یعنی پیکسل‌هایی که پشت پیکسل‌های دیگر هستند ترسیم نمی‌شوند. می‌توانیم این ویژگی را تقریباً به همان سادگی که حذف را با

gl.enable(gl.DEPTH_TEST);

همچنین باید میان‌گیر عمق را قبل‌از شروع طراحی به ۱٫۰ برگردانیم.

// 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‏ ۳ است

برای کسانی از شما که به جزئیات توجه دارید، ممکن است متوجه شده باشید که ما ۲ مشخصه خود را به‌صورت زیر تعریف کردیم

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

عدد «۳» در هرکدام از آن‌ها می‌گوید که فقط ۳ مقدار برای هر مشخصه استخراج شود. این کار می‌کند زیرا در سایه‌زن رأس WebGL پیش‌فرض‌هایی را برای آن‌هایی که شما ارائه نمی‌دهید فراهم می‌کند. مقادیر پیش‌فرض ۰، ۰، ۰، ۱ است که در آن x = 0،‏ y = 0،‏ z = 0 و w = 1. به همین دلیل است که در سایه‌زن رأس دوبعدی قدیمی‌مان باید صریحاً مقدار ۱ را ارائه می‌کردیم. ما x و y را ارسال می‌کردیم و به ۱ برای z نیاز داشتیم اما چون مقدار پیش‌فرض برای z صفر است، باید صریحاً ۱ را ارائه می‌کردیم. اما برای سه‌بعدی، حتی اگر «w» را ارائه نکنیم، به‌طور پیش‌فرض روی ۱ تنظیم می‌شود که همان چیزی است که برای کار کردن ریاضی ماتریس نیاز داریم.