WebGL অর্থোগ্রাফিক 3D

গ্রেগ টাভারেস
Gregg Tavares

WebGL অর্থোগ্রাফিক 3D

এই পোস্টটি WebGL সম্পর্কে পোস্টের একটি ধারাবাহিকতা। প্রথমটি মৌলিক বিষয়গুলি দিয়ে শুরু হয়েছিল এবং আগেরটি ছিল 2D ম্যাট্রিক্স সম্পর্কে 2D ম্যাট্রিক্স । আপনি যদি সেগুলি না পড়ে থাকেন তবে প্রথমে সেগুলি দেখুন। শেষ পোস্টে আমরা 2d ম্যাট্রিক্স কিভাবে কাজ করে তা দেখেছি। আমরা অনুবাদ, ঘূর্ণন, স্কেলিং এবং এমনকি পিক্সেল থেকে ক্লিপ স্পেসে প্রজেক্ট করার বিষয়ে কথা বলেছি 1 ম্যাট্রিক্স এবং কিছু ম্যাজিক ম্যাট্রিক্স ম্যাথ দ্বারা করা যেতে পারে। 3D করা সেখান থেকে একটি ছোট পদক্ষেপ মাত্র। আমাদের পূর্ববর্তী 2D উদাহরণে আমাদের 2D পয়েন্ট (x, y) ছিল যা আমরা একটি 3x3 ম্যাট্রিক্স দ্বারা গুণ করেছি। 3D করতে আমাদের 3D পয়েন্ট (x, y, z) এবং একটি 4x4 ম্যাট্রিক্স প্রয়োজন। আসুন আমাদের শেষ উদাহরণটি গ্রহণ করি এবং এটিকে 3D তে পরিবর্তন করি। আমরা আবার একটি F ব্যবহার করব কিন্তু এবার একটি 3D 'F'। আমাদের প্রথমে যা করতে হবে তা হল 3D হ্যান্ডেল করতে ভার্টেক্স শেডার পরিবর্তন করা। এখানে পুরানো 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>

এটা আরও সহজ হয়েছে! তারপর আমাদের 3D ডেটা সরবরাহ করতে হবে।

...

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

এরপরে আমাদেরকে 2D থেকে 3D তে সমস্ত ম্যাট্রিক্স ফাংশন পরিবর্তন করতে হবে এখানে makeTranslation, makeRotation এবং makeScale এর 2D (আগে) সংস্করণ রয়েছে

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

এবং এখানে আপডেট 3D সংস্করণ আছে.

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টি ঘূর্ণন ফাংশন আছে। আমাদের শুধুমাত্র 2D তে একটির প্রয়োজন ছিল কারণ আমরা কার্যকরভাবে শুধুমাত্র Z অক্ষের চারপাশে ঘুরছিলাম। এখন যদিও 3D করতে আমরা x অক্ষ এবং y অক্ষের চারপাশেও ঘুরতে সক্ষম হতে চাই। আপনি তাদের দিকে তাকানো থেকে দেখতে পারেন তারা সব খুব একই রকম। আমরা যদি সেগুলি বের করি তাহলে আপনি দেখতে পাবেন আগের মতোই সরলীকৃত হয়েছে৷

Z ঘূর্ণন

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

Y ঘূর্ণন


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

এক্স ঘূর্ণন

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

যা পিক্সেল থেকে ক্লিপ স্পেসে রূপান্তরিত হয়েছে। এটিকে 3D তে প্রসারিত করার প্রথম প্রচেষ্টার জন্য আসুন চেষ্টা করি

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 থেকে উচ্চতা পিক্সেল লম্বা, তবে গভীরতার জন্য এটি হবে -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 যা যেকোনো 3D দেখতে কঠিন করে তোলে। এটি ঠিক করতে আসুন জ্যামিতিকে 3D তে প্রসারিত করি। আমাদের বর্তমান F 3টি আয়তক্ষেত্র, 2টি ত্রিভুজ দিয়ে তৈরি। এটিকে 3D করতে মোট 16টি আয়তক্ষেত্রের প্রয়োজন হবে। যে এখানে তালিকা আউট বেশ কয়েক. 16 আয়তক্ষেত্র x 2 ত্রিভুজ প্রতি আয়তক্ষেত্র x 3 শীর্ষবিন্দু প্রতি ত্রিভুজ 96 শীর্ষবিন্দু। আপনি তাদের সব দেখতে চান নমুনা উপর উৎস দেখুন. আমরা তাই আরো শীর্ষবিন্দু আঁকা আছে

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

স্লাইডারগুলি সরানো যে এটি 3D তা বলা বেশ কঠিন। আসুন প্রতিটি আয়তক্ষেত্রকে আলাদা রঙ করার চেষ্টা করি। এটি করার জন্য আমরা আমাদের ভার্টেক্স শেডারে আরেকটি অ্যাট্রিবিউট যোগ করব এবং এটি ভার্টেক্স শেডার থেকে ফ্র্যাগমেন্ট শেডারে পাস করার জন্য আলাদা। এখানে নতুন ভার্টেক্স শেডার আছে

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

ওহ, এটা কি গোলমাল? ঠিক আছে, দেখা যাচ্ছে যে 3D 'F' এর সমস্ত বিভিন্ন অংশ, সামনে, পিছনে, পাশ ইত্যাদি আমাদের জ্যামিতিতে প্রদর্শিত ক্রম অনুসারে আঁকা হয়। এটি আমাদের বেশ পছন্দসই ফলাফল দেয় না কারণ কখনও কখনও পিছনের অংশগুলি সামনেরগুলির পরে টানা হয়৷ WebGL-এর ত্রিভুজগুলির সামনের দিকে এবং পিছনের দিকে মুখ করার ধারণা রয়েছে৷ একটি সম্মুখমুখী ত্রিভুজটির শীর্ষবিন্দুগুলি ঘড়ির কাঁটার দিকে যায়। একটি পিছনের দিকের ত্রিভুজটির শীর্ষবিন্দুগুলি ঘড়ির কাঁটার বিপরীত দিকে যায়।

ত্রিভুজ উইন্ডিং।

WebGL এর শুধুমাত্র সামনের দিকে বা পিছনের দিকের ত্রিভুজ আঁকার ক্ষমতা আছে। আমরা এর সাথে সেই বৈশিষ্ট্যটি চালু করতে পারি

gl.enable(gl.CULL_FACE);

যা আমরা শুধু একবার করি, আমাদের প্রোগ্রামের শুরুতে। এই বৈশিষ্ট্যটি চালু করার সাথে সাথে, WebGL ডিফল্ট ত্রিভুজগুলির পিছনে "কলিং" করে। এই ক্ষেত্রে "Culling" "নট অঙ্কন" এর জন্য একটি অভিনব শব্দ। মনে রাখবেন যে যতদূর WebGL কে বিবেচনা করা হয়েছে, একটি ত্রিভুজ ঘড়ির কাঁটার দিকে যাচ্ছে বা ঘড়ির কাঁটার বিপরীত দিকে যাচ্ছে কিনা তা নির্ভর করে ক্লিপস্পেসে সেই ত্রিভুজের শীর্ষবিন্দুর উপর। অন্য কথায়, আপনি ভার্টেক্স শেডারে শীর্ষবিন্দুতে গণিত প্রয়োগ করার পরে WebGL বের করে যে একটি ত্রিভুজ সামনে আছে নাকি পিছনে। এর মানে হল উদাহরণ স্বরূপ একটি ঘড়ির কাঁটার দিকে ত্রিভুজ যাকে X-এ স্কেল করা হয় তা ঘড়ির কাঁটার বিপরীত ত্রিভুজ হয়ে যায় বা X বা Y অক্ষের চারপাশে 180 ডিগ্রি ঘোরানো ঘড়ির কাঁটার ত্রিভুজটি ঘড়ির কাঁটার বিপরীত ত্রিভুজ হয়ে যায়। যেহেতু আমাদের CULL_FACE অক্ষম ছিল আমরা ঘড়ির কাঁটার দিকে (সামনে) এবং ঘড়ির কাঁটার বিপরীত দিকে (পিছনে) উভয় ত্রিভুজ দেখতে পারি। এখন যেহেতু আমরা এটি চালু করেছি, যে কোনো সময় স্কেলিং বা ঘূর্ণনের কারণে বা যে কোনো কারণেই সামনের দিকের ত্রিভুজটি উল্টে যায়, WebGL এটি আঁকবে না। এটি একটি ভাল জিনিস যেহেতু আপনি 3D তে কিছু ঘুরিয়ে আনবেন আপনি সাধারণত চান যে কোন ত্রিভুজগুলি আপনার মুখোমুখি হয় সেগুলিকে সামনের দিকে বিবেচনা করা হয়।

আরে! কোথায় গেল সব ত্রিভুজ? দেখা যাচ্ছে, তাদের অনেকেই ভুল পথে হাঁটছেন। এটি ঘোরান এবং আপনি অন্য দিকে তাকালে সেগুলি উপস্থিত দেখতে পাবেন। ভাগ্যক্রমে এটি ঠিক করা সহজ। আমরা শুধু দেখি কোনগুলো পশ্চাৎপদ এবং তাদের 2টি শীর্ষবিন্দু বিনিময় করে। উদাহরণস্বরূপ যদি একটি পশ্চাৎমুখী ত্রিভুজ হয়

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

আমরা শুধু শেষ 2টি শীর্ষবিন্দু ফ্লিপ করি যাতে এটি এগিয়ে যায়।

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

এটি কাছাকাছি কিন্তু এখনও একটি সমস্যা আছে. এমনকি সমস্ত ত্রিভুজ সঠিক দিকের দিকে মুখ করে এবং পিছনের দিকের মুখগুলিকে কেটে ফেলার পরেও আমাদের কাছে এখনও এমন জায়গা রয়েছে যেখানে পিছনে থাকা ত্রিভুজগুলি সামনে থাকা উচিত এমন ত্রিভুজগুলির উপরে আঁকা হচ্ছে। DEPTH বাফার লিখুন। একটি গভীরতা বাফার, যাকে কখনও কখনও জেড-বাফার বলা হয়, এটি depth পিক্সেলের একটি আয়তক্ষেত্র, চিত্র তৈরি করতে ব্যবহৃত প্রতিটি রঙের পিক্সেলের জন্য একটি গভীরতার পিক্সেল। WebGL প্রতিটি রঙের পিক্সেল আঁকলে এটি একটি গভীরতার পিক্সেলও আঁকতে পারে। এটি Z এর জন্য ভার্টেক্স শেডার থেকে আমরা যে মানগুলি ফেরত দিয়েছি তার উপর ভিত্তি করে এটি করে। ঠিক যেমন আমাদেরকে X এবং Y-এর জন্য ক্লিপ স্পেস-এ রূপান্তর করতে হয়েছিল, তেমনি Z ক্লিপ স্পেসে বা (-1 থেকে +1)। সেই মানটি তখন একটি গভীর স্থানের মান (0 থেকে +1) এ রূপান্তরিত হয়। WebGL একটি রঙের পিক্সেল আঁকার আগে এটি সংশ্লিষ্ট গভীরতার পিক্সেল পরীক্ষা করবে। যে পিক্সেলটি আঁকতে চলেছে তার গভীরতার মান যদি সংশ্লিষ্ট গভীরতার পিক্সেলের মানের থেকে বেশি হয় তাহলে WebGL নতুন রঙের পিক্সেল আঁকে না। অন্যথায় এটি আপনার ফ্র্যাগমেন্ট শেডার থেকে রঙের সাথে নতুন রঙের পিক্সেল উভয়ই আঁকে এবং এটি নতুন গভীরতার মান সহ গভীরতার পিক্সেল আঁকে। এর মানে, অন্যান্য পিক্সেলের পিছনে থাকা পিক্সেলগুলি আঁকা হবে না। আমরা এই বৈশিষ্ট্যটি প্রায় ততটাই চালু করতে পারি যেভাবে আমরা culling চালু করেছি

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। এই কারণেই আমাদের পুরানো 2D ভার্টেক্স শেডারে আমাদের স্পষ্টভাবে 1 সরবরাহ করতে হয়েছিল। আমরা x এ পাস করছিলাম এবং y এবং আমাদের z এর জন্য একটি 1 দরকার ছিল কিন্তু z এর জন্য ডিফল্ট 0 হওয়ায় আমাদের স্পষ্টভাবে একটি 1 সরবরাহ করতে হয়েছিল। যদিও 3D এর জন্য, এমনকি যদিও আমরা একটি 'w' সরবরাহ করি না এটি ডিফল্ট 1 যা ম্যাট্রিক্স গণিত কাজ করার জন্য আমাদের প্রয়োজন।