WebGL املای سه بعدی

گرگ تاوارس
Gregg Tavares

WebGL Orthographic 3D

این پست ادامه یک سری پست در مورد WebGL است. اولی با اصول شروع شد و قبلی در مورد ماتریس های 2 بعدی در مورد ماتریس های 2 بعدی بود. اگر آنها را نخوانده اید، لطفاً ابتدا آنها را مشاهده کنید. در آخرین پست به نحوه کار ماتریس های 2 بعدی پرداختیم. ما در مورد ترجمه صحبت کردیم، چرخش، مقیاس‌بندی، و حتی نمایش از پیکسل‌ها به فضای کلیپ همگی می‌توانند با 1 ماتریس و مقداری ریاضی ماتریس جادویی انجام شوند. انجام سه بعدی تنها یک قدم کوچک از آنجاست. در مثال‌های دو بعدی قبلی، نقاط دو بعدی (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);
}

بعد باید همه توابع ماتریس را از 2 بعدی به 3 بعدی تغییر دهیم در اینجا نسخه های 2 بعدی (قبل از) 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 تا ارتفاع پیکسل بلند، اما برای عمق -depth / 2 تا +depth / 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);

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

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

و ما باید از آن رنگ در قسمت shader استفاده کنیم

<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 با 1- مقیاس شده است به یک مثلث خلاف جهت عقربه‌های ساعت تبدیل می‌شود یا یک مثلث در جهت عقربه‌های ساعت که 180 درجه حول محور X یا Y می‌چرخد، به یک مثلث خلاف جهت عقربه‌های ساعت تبدیل می‌شود. از آنجایی که ما CULL_FACE را غیرفعال کرده بودیم، می‌توانیم مثلث را در جهت عقربه‌های ساعت (جلو) و خلاف جهت عقربه‌های ساعت (عقب) ببینیم. اکنون که آن را روشن کرده‌ایم، هر زمانی که یک مثلث رو به جلو به دلیل مقیاس‌گذاری یا چرخش یا به هر دلیلی بچرخد، WebGL آن را نمی‌کشد. این یک چیز خوب است زیرا وقتی شما چیزی را به صورت سه بعدی می چرخانید، معمولاً می خواهید که مثلث هایی که روبروی شما هستند رو به جلو در نظر گرفته شوند.

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

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

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

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

این نزدیک تر است اما هنوز یک مشکل دیگر وجود دارد. حتی با وجود اینکه همه مثلث‌ها در جهت درست رو به رو هستند و مثلث‌های رو به عقب حذف می‌شوند، ما هنوز مکان‌هایی داریم که مثلث‌هایی که باید در پشت باشند، روی مثلث‌هایی که باید در جلو باشند، کشیده می‌شوند. DEPTH BUFFER را وارد کنید. بافر عمق، که گاهی اوقات Z-Buffer نامیده می شود، مستطیلی از پیکسل های depth است، یک پیکسل عمق برای هر پیکسل رنگی که برای ساخت تصویر استفاده می شود. همانطور که WebGL هر پیکسل رنگی را ترسیم می کند، می تواند یک پیکسل عمق نیز ترسیم کند. این کار را بر اساس مقادیری که از سایه‌زن رأس برای 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 مقدار در هر ویژگی. این کار به این دلیل کار می‌کند که در سایه‌زن راس، WebGL پیش‌فرض‌هایی را برای کسانی که شما ارائه نمی‌دهید فراهم می‌کند. پیش‌فرض‌ها 0، 0، 0، 1 هستند که در آن x = 0، y = 0، z = 0 و w = 1 است. به همین دلیل است که در سایه‌زن راس دوبعدی قدیمی‌مان باید به صراحت 1 را عرضه می‌کردیم. ما از x عبور می‌کردیم و y و ما به یک 1 برای z نیاز داشتیم، اما چون پیش‌فرض z 0 است، باید به صراحت یک عدد 1 را ارائه می‌کردیم. اگرچه برای سه بعدی، حتی اگر یک "w" را ارائه نمی‌دهیم، به طور پیش‌فرض به 1 می‌رسد که همان چیزی است که برای ماتریس ریاضی به کار.