WebGL و OpenGL
این روزها دنیای وب و مرورگرها، همانطور که از یکی از آنها برای دیدن از این صفحه استفاده میکنید، به بخش جداییناپذیری از زندگی ما تبدیل شده است. برای پیادهسازی مرورگرهای اینترنتی کنسرسیوم وب جهان گستر و سایر نهادهای معتبر استانداردهای متعددی را ارائه میدهند. اعضای نویسندهٔ این استانداردها معمولاً شرکتهای بزرگ اینترنتی مرتبط با وب و خصوصاً شرکتهایی که خود مرورگرها را ایجاد میکنند هستند.
یکی از این استانداردها، استاندارد WebGL است که توسط گروه ناسودبر Khronos برای وب به عنوان یک استاندارد باز ارائه شده است. OpenGL محصول دیگر این گروه است و WebGL از نظر طراحی شباهت بسیاری با آن دارد. محصول رقیب که معمولاً به همراه OpenGL مطرح میشود، DirectX است.
DirectX، توسعه یافته توسط مایکروسافت، بر خلاف OpenGL محدود به استفاده در ویندوز است. حال با استفاده از یک استاندارد باز مانند OpenGL و WebGL و سایر استاندارد وبی شما هیچگاه انحصار به شرکت خاص پیدا نمیکنید.
DirectX به غیر رابطی برای برنامهنویسی گرافیک، امکانات دیگری را از جملهٔ DirectWrite، امکانات مربوط به نمایش متن، و DirectInput امکانات مربوط به دستگاههای ورودی مانند Gamepad و غیره ارائه میدهد. Direct3D قسمت معادل با OpenGL در DirectX است و بهتر از هنگام مقایسهٔ OpenGL نام آن را به جای DirectX بیاوریم.
علاوه بر شباهتهای طراحی بین WebGL و استاندارد OpenGL ES، هر دو استانداردی باز و رایگان هستند. هر دوی آنها توصیفی از یک پیادهسازی هستند و نه خود یک پیادهسازی، به همین دلیل ممکن است اختلافهایی در عملکرد برنامهها در دستگاههای مختلف مشاهده کنید و برنامهای برای یک پیادهسازی نوشته باشید در پیادهسازی دیگر به شکل مطلوب اجرا نشود به همین شاید بهتر باشد عبارت کتابخانه را دربارهٔ آنها به کار نبریم هر چند که GL مخفف Graphic Library است.
همانطور که در ادامهٔ متن خواهیم دید، WebGL رابطی سطح پایین است به این معنی که کار با آن برای رسیدن به نتیجهٔ مطلوب شاید سخت باشد؛ در عوض سرعت بالایی در اختیارتان قرار میدهد اگر که بتوانید برنامهٔ مورد نظرتان را با آن بنویسید. اگر همین الآن میخواهید یک بازی طراحی کنید شاید ایدهٔ بهتری باشد که به جای استفادهٔ مستقیم از این رابط سطح پایین از یک Graphic Engine مانند Ogre یا یک Game Engine مانند Godot یا Unity3d یا Unreal Engine بهره بگیرید ولی مزیت کار با یک رابط سطح پایین، آشنایی با جزئیات سطح پایین و در اینجا آشنایی با پردازندهٔ گرافیک، GPU، و جزئیات و در حقیقت دردسرهای پیادهسازی بازیها است.
پردازندهٔ گرافیکی، بر خلاف پردازندهٔ مرکزی CPU رایانهتان که در برنامهنویسیها پروژههای مختلف شاید با آن کار داشتهاید (مثلاً ایجاد یک برنامهٔ حسابداری)، به دلیل داشتن کاربردی متفاوت، نحوهٔ استفادهٔ متفاوتی دارد که هنگام کار با WebGL، آن را حس خواهید کرد.
حال یک سوال؛ یک پردازنده/کارت گرافیک امروزی یک رایانهٔ رومیزی دارای چندصد پردازنده است، برنامههای کامپیوتری و برنامهنویسان چگونه از قابلیتهای این همه پردازنده کوچک استفاده میکنند؟ جواب، با نوشتن Shader
شیدرها یکی از پایهایترین اجزای ایجاد برنامههای گرافیکی سهبعدی هستند و وجود آنهاست که ایجاد بازیهای گرافیکی روز را ممکن کرده است.
شیدر در لغت به معنی سایهزن است ولی تعریفی که من علاقه دارم بگویم این است؛ شیدرها برنامههایی هستند که ما آنها را به زبان کارت گرافیکی، برای اجرا در آنها مینویسم که کارت گرافیک آنها را با قدرت بالای، سریع و به صورت موازی اجرا کند. سادهتر! واحدهایی نوشتهشده از برنامههایی که قرار است در کارت گرافیک اجرا شوند. سادهتر! برنامههایی که برنامهنویس مینویسد برای اینکه در کارت گرافیک اجرا شود.
شیدرها در OpenGL به زبان GLSL نوشته میشوند که زبانی شبیه به زبان C است. گونههای مختلفی از شیدرها را میتوان برای یک برنامه متصور شد که تفاوت آنها در هنگامی است که قرار است استفاده و اجرا شوند. دو نوع اصلی شیدر عبارتند از Vertex Shader، که قرار است روی گوشهها اجرا شود و Fragment Shader، برنامهای که برای مشخص شدن مقدار هر نقطه اجرا شود.
بسیار خب، نوبت شما است که دست به کار شوید، کد پایین را در به کمک ویرایشگر متنی مورد علاقهتان (مثلاً Notepad)، در فایلی با پسوند html، مثلاً webgl.html ذخیره کنید و آن را در مرورگری جدید مانند کروم یا فایرفاکس باز کنید. در این مثال سعی شده با کمترین مقدار کد، یک خروجی ملموس از رابط برنامهنویسی WebGL ارائه شود.
<canvas id="canvas" width="640" height="480"></canvas>
<script>
var canvas = document.getElementById('canvas');
var gl = canvas.getContext('webgl');
// Build a vertex shader
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, `
precision mediump float;
attribute vec3 pos;
varying vec3 colour;
uniform float time;
void main() {
gl_Position = vec4(
pos.x + sin(time * 2.0) / 2.0,
pos.y + cos(time * 2.0) / 2.0,
pos.z,
1.0
);
}
`);
gl.compileShader(vertexShader);
// Build a fragment shader
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, `
precision mediump float;
uniform float time;
void main() {
gl_FragColor = vec4(
sin(time) / 2.0 + 0.5,
cos(time) / 2.0 + 0.5,
sin(-time) / 2.0 + 0.5,
1.0
);
}
`);
gl.compileShader(fragmentShader);
// Link vertex and fragment shader to form a program, a.k.a. graphic pipeline
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
// Create a uniform, a shared variable between CPU and GPU
var time = gl.getUniformLocation(program, 'time');
var vertexBufferAttribute = gl.getAttribLocation(program, 'pos');
// Create and send our model to GPU as a buffer
// http://antongerdelan.net/opengl/images/colour_buffer.png
var vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.5, 0.0,
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0,
]), gl.STATIC_DRAW);
// gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// Our game loop, a game loop should be as light as possible
setInterval(function () {
// Clear our screen
gl.clearColor(0.3, 0.3, 0.3, 1); // R, G, B, A
gl.clear(gl.COLOR_BUFFER_BIT);
// Select a pipeline, we created only one but on a real application there could many
gl.useProgram(program);
// Select a sent buffer as a model
gl.enableVertexAttribArray(vertexBufferAttribute);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// location, size, type, normalizeFlag, strideToNextPieceOfData, offsetIntoBuffer
gl.vertexAttribPointer(vertexBufferAttribute, 3, gl.FLOAT, false, 0, 0);
// Set time on our pipeline so our GPU can have an understanding of time
gl.uniform1f(time, (Date.now() / 1000) % 10000);
// Now that everything is set, draw our shape which has 3 vertices
gl.drawArrays(gl.TRIANGLES, 0, 3); // primitiveType, offset, count
// What gl.TRIANGLES mean? http://antongerdelan.net/opengl/images/draw_modes.png
}, 1000 / 60);
// "1000 / 60" means 60 times per second, 60 Frame Per Second (FPS)
</script>
این پست هنوز کامل نیست، باید سعی کنم زمانی این پست را با جزئیات کد کامل کنم…
آنچه که درون setInterval آمده، به اصطلاح Game loop نام دارد. کل هدف توسعه و بهبود رابطهای گرافیکی کاهش تعداد و زمان دستورهای مورد نیاز در آن است. یکی از کارهایی که در این رابطه انجام شده، منسوخ شدن Immediate Mode در نسخههای جدید OpenGL است.
در OpenGL 1 با دستور glVertex1f میشد گوشههای یک مدل را در یک صحنه توصیف کنید ولی از آنجایی که اینکار باعث میشد GPU هر دفعه مجبور به انتظار برای رسیدن تکتک دستورات باشد این روش عملاً جلوی استفاده از توان بالای پردازندهٔ گرافیکی را میگرفت. این روش که Immediate Mode نام دارد، منسوخ و بسیار ناکارآمد و مربوط به دههٔ ۹۰ میلادی است! متأسفانه هنوز هم جزوهها و کلاسهایی وجود دارد که این روش را تدریس میکنند. خبر خوب اینکه WebGL اصلاً امکان استفاده از آن را به شما نمیدهد و شما را مجبور میکند که از شیوهٔ درست از کارت گرافیکتان استفاده کنید :)
در حالت جدیدی که در این کد هم استفاده شده، شما باید اطلاعات گوشهها را در Buffer بریزید و هر دفعه با ارجاع به بافر، اطلاعات مختصات گوشهها را به گرافیک جهت کشیدن معرفی کنید. به همین دلیل است که دیگر خبری از glVertex1f و glColor4f نیست! :)
تلاشهای دیگری هم برای کاهش تعداد دستورات درون Game loop وجود داشته و خواهد داشت. با WebGL 2 امکان استفاده از Vertex Buffer Attribute وجود دارد که نیاز نیست هر دفعه Bufferها را به ویژگیهای گوشهها متصل کنید. یا در Vulkan، نسخهٔ از نو نوشته شدهٔ OpenGL، میتوانید صفهایی از دستورها برای گرافیک معرفی کنید و هر دفعه دستورهای معرفی شده در صف را با یک دستور فقط اجرا کنید.
هدف همهٔ این تلاشها کوچک کردن قسمت اجرایی برنامه، آزاد کردن سیپییو و استفاده بهتر از توان پردازشی کارت گرافیک است.