دعونا نبني محرك رسومات ثلاثية الأبعاد: تنقيط الخطوط الخطية والدوائر
() translation by (you can also view the original English article)
مرحباً ، هذا هو الجزء الرابع من سلسلتنا على محركات الرسومات ثلاثية الأبعاد. في هذه المرة ، سنقوم بتغطية التنقيط: عملية أخذ شكل موصوف بواسطة صيغ رياضية وتحويله إلى صورة على شاشتك.
تلميح: تم إنشاء جميع المفاهيم الموجودة في هذه المقالة خارج الفصول الدراسية التي أنشأناها في المشاركات الثلاث الأولى ، لذا تأكد من التحقق منها أولاً.
خلاصة
فيما يلي مراجعة للصفوف التي قمنا بها حتى الآن:
1 |
|
2 |
Point Class |
3 |
{
|
4 |
Variables:
|
5 |
num tuple[3]; //(x,y,z) |
6 |
Operators:
|
7 |
Point AddVectorToPoint(Vector); |
8 |
Point SubtractVectorFromPoint(Vector); |
9 |
Vector SubtractPointFromPoint(Point); |
10 |
Null SetPointToPoint(Point); |
11 |
Functions:
|
12 |
drawPoint; //draw a point at its position tuple |
13 |
}
|
14 |
|
15 |
Vector Class |
16 |
{
|
17 |
Variables:
|
18 |
num tuple[3]; //(x,y,z) |
19 |
Operators:
|
20 |
Vector AddVectorToVector(Vector); |
21 |
Vector SubtractVectorFromVector(Vector); |
22 |
Vector RotateXY(degrees); |
23 |
Vector RotateYZ(degrees); |
24 |
Vector RotateXZ(degrees); |
25 |
Vector Scale(s0,s1,s2); //receives a scaling 3-tuple, returns the scaled vector |
26 |
}
|
27 |
|
28 |
Camera Class |
29 |
{
|
30 |
Vars:
|
31 |
int minX, maxX; |
32 |
int minY, maxY; |
33 |
int minZ, maxZ; |
34 |
array objectsInWorld; //an array of all existent objects |
35 |
Functions:
|
36 |
null drawScene(); //draws all needed objects to the screen |
37 |
}
|
يمكنك الاطلاع على نموذج البرنامج من الجزء الثالث من السلسلة لمعرفة كيفية عملهم معًا.
الآن ، دعونا نلقي نظرة على بعض الأشياء الجديدة!
التنقيط
التنقيط (أو البكسلة، إذا كنت ترغب في ذلك) هو عملية أخذ شكل موصوف في تنسيق رسومات متجه (أو في حالتنا ، رياضياً) وتحويلها إلى صورة نقطية (حيث يتم احتواء الشكل على بنية بكسل).
نظرًا لأن الرياضيات ليست دائمًا بالدقة التي نحتاجها لاستخدامها في رسومات الكمبيوتر ، يجب أن نستخدم الخوارزميات لتلائم الأشكال التي تصفها على الشاشة الصحيحة. على سبيل المثال ، يمكن أن تقع نقطة على التنسيق \\ ((3.2 ، 4.6) \\) في الرياضيات ، ولكن عندما نعرضها ، يجب علينا دفعها إلى \\ ((3 ، 5) \\) حتى يمكن وضعها في هيكل بكسل لشاشتنا.
سيكون لكل نوع من الأشكال التي نقوم بتنقيطها خوارزمية خاصة بها للقيام بذلك. لنبدأ بأحد الأشكال الأكثر بساطة للتنقيط: مقطع الخط.
سطر القطعة



تعتبر المقاطع الخطية واحدة من أبسط الأشكال التي يمكن رسمها ، ولذلك غالبًا ما تكون واحدة من أول الأشياء التي يتم تغطيتها في أي فئة من أشكال الهندسة. يتم تمثيلها بنقطتين متميزتين (نقطة بداية واحدة ونقطة نهاية واحدة) ، والخط الذي يربط بين الاثنين. تسمى الخوارزمية الأكثر شيوعًا في تنقيط مقطع الخط خوارزمية Bresenham.
خطوة بخطوة ، تعمل خوارزمية Bresenham على النحو التالي:
- استلم نقطة البداية ونقطة النهاية لقطعة الخط كمدخل.
- تحديد الاتجاه مقطع خط بتحديد خصائصه \(dx\) و \(dy\) (\ (dx = x_ {1}-x_ {0} \)، \ (dy = y_ {1}-y_ {0} \)).
- تحديد
sx
،sy
, و ، سنلقي نظرة على بعض من أفضل تقويم وجدولة التطبيقات المتاحة عبر الإنترنت. - تقريب كل نقطة في جزء السطر إلى البكسل الموجود أعلاه أو أسفله.
قبل أن ننفذ خوارزمية Bresenham ، دعنا نجمع فئة من الشرائح الأساسية لاستخدامها في محركنا:
1 |
|
2 |
LineSegment Class |
3 |
{
|
4 |
Variables:
|
5 |
int startX, startY; //the starting point of our line segment |
6 |
int endX, endY; //the ending point of our line segment |
7 |
Function:
|
8 |
array returnPointsInSegment; //all points lying on this line segment |
9 |
}
|
إذا كنت تريد إجراء تحويل على صف LineSegment
الجديد ، فكل ما عليك فعله هو تطبيق التحويل الذي اخترته على نقاط البداية والنهاية لـ LineSegment
ثم وضعها مرة أخرى في الفصل. ستتم معالجة جميع النقاط التي تقع بين النقاط عندما يتم رسم LineSegment
نفسه ، حيث تتطلب خوارزمية Bresenham نقطة البداية والنهاية فقط للعثور على كل نقطة لاحقة.
لكي تتناسب فئة LineSegment
مع محركنا الحالي ، لا يمكننا أن نمتلك بالفعل دالة draw()
مضمنة في الفصل ، ولهذا السبب اخترت استخدام وظيفة returnPointsInSegment
بدلاً من ذلك. ستقوم هذه الدالة بإرجاع مصفوفة من كل نقطة موجودة داخل مقطع الخط ، مما يسمح لنا برسم الجزء الخطي وإعدامه بالشكل المناسب.
تبدو الدالة returnPointsInSegment()
تشبه إلى حد ما هذا (في JavaScript):
1 |
|
2 |
function returnPointsInSegment() |
3 |
{
|
4 |
//create a list to store all of the line segment's points in
|
5 |
var pointArray = new Array(); |
6 |
//set this function's variables based on the class's starting and ending points
|
7 |
var x0 = this.startX; |
8 |
var y0 = this.startY; |
9 |
var x1 = this.endX; |
10 |
var y1 = this.endY; |
11 |
//define vector differences and other variables required for Bresenham's Algorithm
|
12 |
var dx = Math.abs(x1-x0); |
13 |
var dy = Math.abs(y1-y0); |
14 |
var sx = (x0 & x1) ? 1 : -1; //step x |
15 |
var sy = (y0 & y1) ? 1 : -1; //step y |
16 |
var err = dx-dy; //get the initial error value |
17 |
//set the first point in the array
|
18 |
pointArray.push(new Point(x0,y0)); |
19 |
|
20 |
//Main processing loop
|
21 |
while(!((x0 == x1) && (y0 == y1))) |
22 |
{
|
23 |
var e2 = err * 2; //hold the error value |
24 |
//use the error value to determine if the point should be rounded up or down
|
25 |
if(e2 => -dy) |
26 |
{
|
27 |
err -= dy; |
28 |
x0 += sx; |
29 |
}
|
30 |
if(e2 < dx) |
31 |
{
|
32 |
err += dx; |
33 |
y0 += sy; |
34 |
}
|
35 |
//add the new point to the array
|
36 |
pointArray.push(new Point(x0, y0)); |
37 |
}
|
38 |
return pointArray; |
39 |
}
|
تتمثل أسهل طريقة لإضافة شرائح الخط في فئة الكاميرا في إضافة بنية بسيطة if
كانت مشابهة لما يلي:
1 |
|
2 |
//loop through array of objects
|
3 |
if (class type == Point) |
4 |
{
|
5 |
//do our current rendering code
|
6 |
}
|
7 |
else if (class type == LineSegment) |
8 |
{
|
9 |
var segmentArray = LineSegment.returnPointsInSegment(); |
10 |
//loop through points in the array, drawing and culling them as we have previously
|
11 |
}
|
هذا هو كل ما تحتاجه للحصول على صفك الدراسي الأول وتشغيله! إذا كنت ترغب في معرفة المزيد عن الجوانب الأكثر تقنية لخوارزمية Bresenham (خصوصًا أقسام الأخطاء) ، يمكنك الاطلاع على مقالة Wikipedia عليه.
الدوائر

تكون عملية تنقيط الدائرة أكثر صعوبة قليلاً من تنقيط جزء من الخط ، ولكن ليس كثيرًا. سنستخدم خوارزمية دائرة المنتصف لنفعل كل رفعنا الثقيل ، وهو امتداد لخوارزمية Bresenham التي سبق ذكرها. على هذا النحو ، فإنه يتبع خطوات مشابهة لتلك المذكورة أعلاه ، مع بعض الاختلافات الطفيفة.
تعمل الخوارزمية الجديدة لدينا على النحو التالي:
- استلم نقطة مركزية ونصف دائرة.
- اضبط النقاط في كل اتجاه أساسي
- دورة من خلال كل من الأرباع ، رسم الأقواس الخاصة بهم
سيكون فصل الدائرة لدينا مشابهًا جدًا لفئة شرائح الخط ، ويبحث عن شيء من هذا القبيل:
1 |
|
2 |
Circle Class |
3 |
{
|
4 |
Variables:
|
5 |
int centerX, centerY; //the center point of our circle |
6 |
int radius; //the radius of our circle |
7 |
Function:
|
8 |
array returnPointsInCircle; //all points within this Circle |
9 |
}
|
تعمل وظيفة returnPointsInCircle()
الخاصة بنا بالطريقة نفسها التي تعمل بها وظيفة صف LineSegment
، حيث تقوم بإرجاع مجموعة من النقاط حتى تتمكن الكاميرا من تقديمها وإعدامها حسب الحاجة. يتيح ذلك لمحركنا التعامل مع مجموعة متنوعة من الأشكال ، مع وجود تغييرات طفيفة فقط مطلوبة لكل منها.
هذا ما ستبدو عليه وظيفة returnPointsInCircle()
الخاصة بنا (في JavaScript):
1 |
|
2 |
function returnPointsInCircle() |
3 |
{
|
4 |
//store all of the circle's points in an array
|
5 |
var pointArray = new Array(); |
6 |
//set up the values needed for the algorithm
|
7 |
var f = 1 - radius; //used to track the progress of the drawn circle (since its semi-recursive) |
8 |
var ddFx = 1; //step x |
9 |
var ddFy = -2 * this.radius; //step y |
10 |
var x = 0; |
11 |
var y = this.radius; |
12 |
|
13 |
//this algorithm doesn't account for the farthest points,
|
14 |
//so we have to set them manually
|
15 |
pointArray.push(new Point(this.centerX, this.centerY + this.radius)); |
16 |
pointArray.push(new Point(this.centerX, this.centerY - this.radius)); |
17 |
pointArray.push(new Point(this.centerX + this.radius, this.centerY)); |
18 |
pointArray.push(new Point(this.centerX - this.radius, this.centerY)); |
19 |
|
20 |
while(x < y) { |
21 |
if(f >= 0) { |
22 |
y--; |
23 |
ddFy += 2; |
24 |
f += ddFy; |
25 |
}
|
26 |
x++; |
27 |
ddFx += 2; |
28 |
f += ddFx; |
29 |
|
30 |
//build our current arc
|
31 |
pointArray.push(new Point(x0 + x, y0 + y)); |
32 |
pointArray.push(new Point(x0 - x, y0 + y)); |
33 |
pointArray.push(new Point(x0 + x, y0 - y)); |
34 |
pointArray.push(new Point(x0 - x, y0 - y)); |
35 |
pointArray.push(new Point(x0 + y, y0 + x)); |
36 |
pointArray.push(new Point(x0 - y, y0 + x)); |
37 |
pointArray.push(new Point(x0 + y, y0 - x)); |
38 |
pointArray.push(new Point(x0 - y, y0 - x)); |
39 |
}
|
40 |
|
41 |
return pointArray; |
42 |
}
|
الآن ، نقوم فقط بإضافة عبارة if
أخرى إلى حلقة الرسم الرئيسية الخاصة بنا ، وهذه الدوائر متكاملة تمامًا!
إليك كيفية ظهور حلقة الرسم المحدثة:
1 |
|
2 |
//loop through array of objects
|
3 |
if(class type == point) |
4 |
{
|
5 |
//do our current rendering code
|
6 |
}
|
7 |
else if(class type == LineSegment) |
8 |
{
|
9 |
var segmentArray = LineSegment.returnPointsInSegment(); |
10 |
//loop through points in the array, drawing and culling them as we have previously
|
11 |
}
|
12 |
else if(class type == Circle) |
13 |
{
|
14 |
var circleArray = Circle.returnPointsInCircle(); |
15 |
//loop through points in the array, drawing and culling them as we have previously
|
16 |
}
|
الآن بعد أن أصبح لدينا صفوف جديدة بعيدة عن الطريق ، دعونا نجعل شيء ما!
ماستر النقطية
برنامجنا سيكون بسيطا هذه المرة. عندما ينقر المستخدم على الشاشة ، سنقوم برسم دائرة تكون نقطة مركزها هي النقطة التي تم النقر عليها ، ويكون نصف قطرها رقمًا عشوائيًا.
دعونا نلقي نظرة على الكود:
1 |
|
2 |
main{ |
3 |
//setup for your favorite Graphics API here
|
4 |
//setup for keyboard input (may not be required) here
|
5 |
|
6 |
var camera = new Camera(); //create an instance of the camera class |
7 |
camera.objectsInWorld[]; //create 100 object spaces within the camera's array |
8 |
|
9 |
//set the camera's view space
|
10 |
camera.minX = 0; |
11 |
camera.maxX = screenWidth; |
12 |
camera.minY = 0; |
13 |
camera.maxY = screenHeight; |
14 |
camera.minZ = 0; |
15 |
camera.maxZ = 100; |
16 |
|
17 |
while(key != esc) { |
18 |
if(mouseClick) { |
19 |
//create a new circle
|
20 |
camera.objectsInWorld.push(new Circle(mouse.x,mouse.y,random(3,10)); |
21 |
//render everything in the scene
|
22 |
camera.drawScene(); |
23 |
}
|
24 |
}
|
25 |
}
|
مع أي حظ ، يجب أن تكون الآن قادرًا على استخدام محرك محدّث لجذب بعض الدوائر الرائعة.
استنتاج
الآن وبعد أن أصبح لدينا بعض ميزات التنقيط الأساسية في محركنا ، يمكننا في النهاية البدء في رسم بعض الأشياء المفيدة على الشاشة! لا شيء معقد للغاية بعد ، ولكن إذا أردت ، يمكنك تجميع بعض الأرقام العصا ، أو شيء من هذا القبيل.
في المشاركة التالية ، سنلقي نظرة أخرى على التنقيط. - سنقوم في هذه المرة فقط بإعداد فئتين أخريين لاستخدامهما في محركنا: مثلثات وأربعة أجيال. ترقب!