حد أدنى من WebGL في 75 سطرًا من التعليمات البرمجية

يختلف برنامج OpenGL الحديث و WebGL بشكل أوسع عن برنامج OpenGL الأقدم الذي درسته في الماضي. أفهم كيفية عمل التنقيط ، لذا فأنا على دراية بالمفاهيم. ومع ذلك ، فإن كل برنامج تعليمي قرأته يقدم أفكارًا مجردة ووظائف مساعدة جعلت من الصعب عليّ فهم الأجزاء التي تنتمي إلى واجهات برمجة تطبيقات OpenGL نفسها.



للتوضيح ، تعد الأفكار المجردة مثل تقسيم هذه المواقف وتقديم الوظائف إلى فئات منفصلة مهمة في تطبيقات العالم الحقيقي. ومع ذلك ، فإن هذه التجريدات تشتت الكود في مناطق مختلفة وتضيف التكرار بسبب الصيغة المعيارية ونقل البيانات بين الوحدات المنطقية. أجد أنه من الأنسب دراسة موضوع في تدفق رمز خطي ، حيث يرتبط كل سطر مباشرة بهذا الموضوع.



أولاً ، أود أن أشكر منشئ البرنامج التعليمي الذي استخدمته . وبأخذها كأساس ، تخلصت من كل الأفكار المجردة حتى حصلت على "الحد الأدنى من البرنامج القابل للتطبيق". آمل أن يساعدك في بدء استخدام OpenGL الحديث. إليك ما سنفعله:





مثلث متساوي الأضلاع ، أخضر في الأعلى ، أسود في أسفل اليسار ، وأحمر في أسفل اليمين ، مع ألوان محرفة بين النقاط. نسخة أكثر إشراقًا من المثلث الأسود [ ترجمة في حبري].



التهيئة



في WebGL ، نحتاج canvasإلى الرسم. بالطبع ، ستحتاج بالتأكيد إلى إضافة جميع أنماط HTML المعتادة ، والأنماط ، وما إلى ذلك ، لكن قماش الرسم هو أهم شيء. بعد تحميل DOM ، يمكننا الوصول إلى اللوحة القماشية باستخدام Javascript.



<canvas id="container" width="500" height="500"></canvas>

<script>
  document.addEventListener('DOMContentLoaded', () => {
    // All the Javascript code below goes here
  });
</script>


من خلال الوصول إلى اللوحة ، يمكننا الحصول على سياق عرض WebGL وتهيئة لونه الواضح. يتم تخزين الألوان في عالم OpenGL على هيئة RGBA ولكل مكون قيمة من 0إلى 1. اللون الصافي هو اللون المستخدم في رسم اللوحة القماشية في بداية كل إطار ، مع إعادة رسم المشهد.



const canvas = document.getElementById('container');
const gl = canvas.getContext('webgl');

gl.clearColor(1, 1, 1, 1);


في البرامج الحقيقية ، يمكن ويجب أن تكون التهيئة أكثر تفصيلاً. على وجه الخصوص ، يجب الإشارة إلى تضمين مخزن مؤقت للعمق يسمح لك بفرز الهندسة بناءً على إحداثيات Z. لن نقوم بذلك لبرنامج بسيط بمثلث واحد فقط.



تجميع التظليل



يعد OpenGL في جوهره إطار عمل تنقيط حيث يتعين علينا تحديد كيفية تنفيذ كل شيء بخلاف التنقيط. لذلك ، يجب تنفيذ خطوتي رمز على الأقل على وحدة معالجة الرسومات:



  1. تظليل رأس يقوم بمعالجة جميع البيانات الواردة ويخرج موضعًا ثلاثي الأبعاد واحدًا (في الواقع موضع رباعي الأبعاد في إحداثيات موحدة ) لكل إدخال.
  2. تظليل شظي يعالج كل بكسل على الشاشة ، ويعرض اللون الذي يجب أن يتم رسم البكسل به.


بين هاتين المرحلتين ، يحصل برنامج OpenGL على الشكل الهندسي من تظليل قمة الرأس ويحدد وحدات البكسل التي تغطيها تلك الهندسة. هذه هي مرحلة التنقيط.



تتم كتابة كلا التظليل عادةً في GLSL (OpenGL Shading Language) ، والتي يتم تجميعها بعد ذلك في رمز الجهاز لوحدة معالجة الرسومات. ثم يتم تمرير رمز الجهاز إلى GPU بحيث يمكن تنفيذه أثناء عملية العرض. لن أخوض في GLSL بالتفصيل لأنني أريد فقط إظهار الأساسيات ، لكن اللغة قريبة بما يكفي من لغة C لتكون مألوفة لمعظم المبرمجين.



أولاً ، نقوم بتجميع وتمرير تظليل الرأس إلى وحدة معالجة الرسومات. في الجزء الموضح أدناه ، يتم تخزين شفرة مصدر shader كسلسلة ، ولكن يمكن تحميلها من أماكن أخرى. أخيرًا ، يتم تمرير السلسلة إلى WebGL API.



const sourceV = `
  attribute vec3 position;
  varying vec4 color;

  void main() {
    gl_Position = vec4(position, 1);
    color = gl_Position * 0.5 + 0.5;
  }
`;

const shaderV = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(shaderV, sourceV);
gl.compileShader(shaderV);

if (!gl.getShaderParameter(shaderV, gl.COMPILE_STATUS)) {
  console.error(gl.getShaderInfoLog(shaderV));
  throw new Error('Failed to compile vertex shader');
}


يجدر شرح بعض المتغيرات في كود GLSL هنا:



  1. (attribute) position. , , .
  2. Varying color. ( ) . .
  3. gl_Position. , , varying-. , ,


يوجد أيضًا نوع متغير منتظم ، وهو ثابت في جميع مكالمات تظليل الرأس. يتم استخدام هذه الزي الرسمي لخصائص مثل مصفوفة التحويل ، والتي ستكون ثابتة لجميع رؤوس عنصر هندسي واحد.



بعد ذلك ، نفعل الشيء نفسه مع تظليل الأجزاء - نقوم بتجميعه ونقله إلى وحدة معالجة الرسومات. لاحظ أن المتغير colorمن تظليل قمة الرأس يتم قراءته الآن بواسطة تظليل الجزء.



const sourceF = `
  precision mediump float;
  varying vec4 color;

  void main() {
    gl_FragColor = color;
  }
`;

const shaderF = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(shaderF, sourceF);
gl.compileShader(shaderF);

if (!gl.getShaderParameter(shaderF, gl.COMPILE_STATUS)) {
  console.error(gl.getShaderInfoLog(shaderF));
  throw new Error('Failed to compile fragment shader');
}


علاوة على ذلك ، يتم ربط كل من تظليل الرأس والجزء في برنامج OpenGL واحد.



const program = gl.createProgram();
gl.attachShader(program, shaderV);
gl.attachShader(program, shaderF);
gl.linkProgram(program);

if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
  console.error(gl.getProgramInfoLog(program));
  throw new Error('Failed to link program');
}

gl.useProgram(program);


نخبر وحدة معالجة الرسومات (GPU) أننا نريد تنفيذ التظليل أعلاه. الآن كل ما يتعين علينا القيام به هو إنشاء البيانات الواردة والسماح لوحدة معالجة الرسومات بمعالجة هذه البيانات.



إرسال البيانات الواردة إلى وحدة معالجة الرسومات



سيتم تخزين البيانات الواردة في ذاكرة GPU ومعالجتها من هناك. بدلاً من إجراء مكالمات سحب منفصلة لكل جزء من البيانات الواردة ، والتي تنقل البيانات المقابلة قطعة واحدة في كل مرة ، يتم نقل جميع البيانات الواردة بالكامل إلى وحدة معالجة الرسومات وقراءتها من هناك. (قام برنامج OpenGL القديم بتمرير البيانات عن العناصر الفردية ، مما أدى إلى إبطاء الأداء.)



يوفر OpenGL فكرة تجريدية تسمى Vertex Buffer Object (VBO). ما زلت أفكر في كيفية عملها ، لكننا سنفعل في النهاية ما يلي لاستخدامه:



  1. تخزين تسلسل البيانات في ذاكرة وحدة المعالجة المركزية (CPU).
  2. بايت نقل إلى ذاكرة GPU من خلال منطقة عازلة فريدة من نوعها تم إنشاؤها باستخدام gl.createBuffer()و نقاط الربط gl.ARRAY_BUFFER .


لكل متغير من بيانات الإدخال (السمة) في تظليل الرأس ، سيكون لدينا VBO واحد ، على الرغم من أنه من الممكن استخدام VBO واحد لعدة عناصر من بيانات الإدخال.



const positionsData = new Float32Array([
  -0.75, -0.65, -1,
   0.75, -0.65, -1,
   0   ,  0.65, -1,
]);

const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, positionsData, gl.STATIC_DRAW);


عادةً ما نحدد الهندسة بأي إحداثيات يفهمها تطبيقنا ، ثم نستخدم مجموعة من التحويلات في تظليل قمة الرأس لتعيينها في مساحة مقطع OpenGL. لن أخوض في التفاصيل حول مساحة الاقتطاع (فهي مرتبطة بإحداثيات موحدة) ، بينما تحتاج فقط إلى معرفة أن X و Y يتغيران في النطاق من -1 إلى +1. نظرًا لأن تظليل الرأس يمرر المدخلات كما هي ، يمكننا ضبط إحداثياتنا مباشرة في مساحة القطع.



ثم سنقوم أيضًا بربط المخزن المؤقت بأحد المتغيرات في تظليل الرأس. في الكود نقوم بما يلي:



  1. نحصل على واصف المتغير positionمن البرنامج الذي تم إنشاؤه أعلاه.
  2. نوجه OpenGL لقراءة البيانات من نقطة الربط gl.ARRAY_BUFFERفي مجموعات من 3 مع معلمات معينة ، على سبيل المثال ، مع إزاحة وخطوة 0.




const attribute = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(attribute);
gl.vertexAttribPointer(attribute, 3, gl.FLOAT, false, 0, 0);


تجدر الإشارة إلى أنه يمكننا إنشاء VBO بهذه الطريقة وربطها بسمة تظليل قمة الرأس لأننا ننفذ هذه الوظائف واحدة تلو الأخرى. إذا كان علينا فصل الوظيفتين (على سبيل المثال ، إنشاء جميع VBO في مسار واحد ، ثم ربطهم بسمات منفصلة) ، ثم قبل تعيين كل VBO إلى السمة المقابلة ، سنحتاج إلى الاتصال في كل مرة gl.bindBuffer(...).



استدعاء!



أخيرًا ، عندما يتم إعداد جميع البيانات الموجودة في ذاكرة GPU بشكل صحيح ، يمكننا إخبار OpenGL بمسح الشاشة وتشغيل البرنامج لمعالجة المصفوفات التي أعددناها. كجزء من خطوة التنقيط (تحديد وحدات البكسل التي تغطيها الرؤوس) ، نخبر OpenGL بمعالجة الرؤوس في مجموعات من 3 كمثلثات.



gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);


مع مثل هذا المخطط الخطي ، سيتم تنفيذ البرنامج دفعة واحدة. في أي تطبيق عملي ، نخزن البيانات بطريقة منظمة ، ونرسلها إلى وحدة معالجة الرسومات أثناء تغيرها ، ونعرضها في كل إطار.






للتلخيص ، يوجد أدناه مخطط يحتوي على مجموعة قليلة من المفاهيم المطلوبة لعرض المثلث الأول على الشاشة. ولكن حتى هذا المخطط تم تبسيطه إلى حد كبير ، لذلك من الأفضل كتابة 75 سطرًا من التعليمات البرمجية المعروضة في هذه المقالة ودراستها.





التسلسل النهائي المبسط للغاية للخطوات المطلوبة لعرض المثلث



بالنسبة لي ، كان أصعب جزء في تعلم OpenGL هو المقدار الهائل من المتغيرات المطلوبة لعرض أبسط صورة على الشاشة. نظرًا لأن إطار عمل التحويل النقطي يتطلب منا توفير وظيفة العرض ثلاثي الأبعاد ، والتواصل مع وحدة معالجة الرسومات كبير جدًا ، يجب دراسة العديد من المفاهيم مباشرة. نأمل أن تكون هذه المقالة قد أوضحت لك الأساسيات بطريقة أبسط مما تظهر في البرامج التعليمية الأخرى.



أنظر أيضا:








أنظر أيضا:






All Articles