3D تفعل ذلك بنفسك. الجزء 1: بكسل وخطوط



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

في الجزء الأول سننظر في:

  • مفاهيم التقديم (البرمجيات ، الأجهزة)
  • ما هو بكسل / السطح؟
  • تحليل مفصل لمخرجات الخط

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

قبل البدء في استكشاف عالم 3D المثير للاهتمام ، دعنا نختار لغة برمجة للأمثلة ، بالإضافة إلى بيئة التطوير. ما اللغة التي يجب أن أختارها لبرمجة الرسومات ثلاثية الأبعاد؟ أي شخص ، يمكنك العمل في أي مكان تشعر فيه بالراحة ، فالرياضيات ستكون هي نفسها في كل مكان. في هذه المقالة ، سيتم عرض جميع الأمثلة في سياق JS (هنا تطير لي الطماطم). لماذا شبيبة؟ الأمر بسيط - في الآونة الأخيرة كنت أعمل معه بشكل رئيسي ، وبالتالي يمكنني نقل الجوهر لك بشكل أكثر فعالية. سأتجاوز جميع ميزات JS في الأمثلة ، لأن نحتاج فقط إلى الميزات الأساسية التي تمتلكها أي لغة ، لذلك سننتبه بشكل خاص إلى الأبعاد الثلاثية. لكنك تختار ما تحب ، لأنه في المقالات ، لن يتم ربط جميع الصيغ بميزات أي لغة برمجة. أي بيئة تختار؟ ليس مهما،في حالة JS ، أي محرر نصوص مناسب ، يمكنك استخدام المحرر الأقرب إليك.

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

جميع الشاشات ثلاثية الأبعاد على الشاشة باستخدام البكسل ، لاحقًا في المقالات ، سترى كيف يحدث ذلك. هل سيتباطأ؟ بدون تسريع الأجهزة (على سبيل المثال ، التسارع بواسطة بطاقة فيديو) - سيكون. في المقالة الأولى ، لن نستخدم التسارع ، سنكتب كل شيء من نقطة الصفر لفهم الجوانب الأساسية للأبعاد ثلاثية الأبعاد. دعونا نلقي نظرة على بعض المصطلحات التي سيتم ذكرها في المقالات المستقبلية:

  • (Rendering) — 3D- . , 3D- , , .
  • (Software Rendering) — . , , , - . , . 3D- , — .
  • عرض الأجهزة - عملية عرض بمساعدة الأجهزة. أستخدمه الألعاب والتطبيقات. كل شيء يعمل بسرعة ، لأنه تستحوذ الكثير من الحوسبة الروتينية على بطاقة الفيديو ، المصممة لهذا الغرض.

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

أولاً ، أنشئ مشروعًا ، فهو بالنسبة لي مجرد ملف index.html نصي ، بالمحتوى التالي:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>3D it’s easy. Part 1</title>
</head>

<body>
    <!--         -->
    <canvas id="surface" width="800" height="600"></canvas>

    <script>
        //    
    </script>
</body>

</html>

لن أركز كثيرًا على JS والقماش الآن - هذه ليست الشخصيات الرئيسية في هذه المقالة. ولكن للحصول على فهم عام ، سأوضح أن <قماش ...> هو مستطيل (في حالتي ، 800 × 600 بكسل في الحجم) سأعرض فيه كل الرسومات. لقد سجلت لوحة الرسم مرة واحدة ولن أغيرها بعد الآن.

<script></script> 

البرنامج النصي - عنصر سنقوم من خلاله بكتابة كل المنطق لتقديم الرسومات ثلاثية الأبعاد بأيدينا (في جافا سكريبت). 

عندما استعرضنا للتو بنية ملف index.html للمشروع الذي تم إنشاؤه حديثًا ، سنبدأ في التعامل مع الرسومات ثلاثية الأبعاد.

عندما نرسم شيئًا ما في النافذة ، يتحول هذا في العد النهائي إلى وحدات بكسل ، لأنها هي التي تعرضها الشاشة. كلما زاد عدد وحدات البكسل ، زادت حدة الصورة ، ولكن يتم أيضًا تحميل الكمبيوتر أكثر. كيف يتم رسم ما نرسمه في النافذة المخزنة؟ يمكن تمثيل الرسومات في أي نافذة كمصفوفة من وحدات البكسل ، والبكسل نفسه هو مجرد لون. أي أن دقة الشاشة 800 × 600 تعني أن نافذتنا تحتوي على 600 سطر من 800 بكسل لكل منها ، وهي 800 * 600 = 480000 بكسل ، كثيرًا ، أليس كذلك؟ يتم تخزين وحدات البكسل في صفيف. دعونا نفكر في أي مجموعة سوف نقوم بتخزين البكسل. إذا كان يجب أن يكون لدينا 800 × 600 بكسل ، فإن الخيار الأكثر وضوحًا هو في صفيف ثنائي الأبعاد 800 × 600. وهذا هو الخيار الصحيح تقريبًا ، أو بالأحرى ، الخيار الصحيح تمامًا. لكن بكسلات النافذة ، من الأفضل التخزين في صفيف أحادي البعد مكون من 480.000 عنصر (إذا كانت الدقة 800 × 600) ،فقط لأنه أسرع للعمل مع مصفوفة أحادية البعد ، لأنه يتم تخزينه في الذاكرة في تسلسل مستمر من وحدات البايت (كل شيء يقع في مكان قريب وبالتالي يسهل الحصول عليه). في صفيف ثنائي الأبعاد (على سبيل المثال ، في حالة JS) ، يمكن أن يتناثر كل سطر في أماكن مختلفة في الذاكرة ، لذا فإن الوصول إلى عناصر هذا الصفيف سيستغرق وقتًا أطول. أيضًا ، للتكرار عبر صفيف أحادي البعد ، يلزم دورة واحدة فقط ، وللأعداد الصحيحة ثنائية الأبعاد 2 ، نظرًا للحاجة إلى إجراء عشرات الآلاف من تكرارات الدورة ، فإن السرعة مهمة هنا. ما هو بكسل في مثل هذا الصفيف؟ كما ذكر أعلاه - هذا مجرد لون ، أو بالأحرى 3 من مكوناته (الأحمر والأخضر والأزرق). أي ، حتى أكثر الصور الملونة هي مجرد مجموعة من البكسل بألوان مختلفة. يمكن تخزين بكسل في الذاكرة كما تريد ، إما صفيف من 3 عناصر ، أو في بنية حيث الأحمر ، gree ،أزرق؛ أو أي شيء آخر. صورة تتكون من مجموعة من البكسلات التي نقوم بتحليلها للتو ، سأستمر في تسمية السطح. اتضح أنه نظرًا لأن كل ما يتم عرضه على الشاشة يتم تخزينه في مصفوفة من وحدات البكسل ، ثم تغيير العناصر (وحدات البكسل) في هذا الصفيف - سنقوم بتغيير الصورة على الشاشة بكسلًا تلو الآخر. هذا هو بالضبط ما سنفعله في هذه المقالة.

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

//     ()    
const ctx = document
.getElementById('surface')
.getContext('2d')

//     ,    
// +       
const imageData = ctx.createImageData(800, 600)

في المثال ، imageData هي كائن يوجد فيه 3 خصائص:

  • الارتفاع والعرض - أعداد صحيحة تخزن ارتفاع وعرض نافذة الرسم
  • البيانات - مصفوفة عدد صحيح 8 بت غير موقعة (يمكنك تخزين الأرقام في النطاق من 0 إلى 255 فيه)

صفيف البيانات له بنية بسيطة ولكنها تفسيرية. يخزن هذا الصفيف أحادي البعد بيانات كل بكسل ، والتي سنعرضها على الشاشة بالتنسيق التالي:
العناصر الأربعة الأولى للصفيف (المؤشرات 0،1،2،3) هي بيانات البكسل الأول في الصف الأول. العناصر الأربعة الثانية (المؤشرات 4 ، 5 ، 6 ، 7) هي بيانات البكسل الثاني من الصف الأول. عندما نصل إلى 800 بكسل للخط الأول ، بشرط أن تكون النافذة بعرض 800 بكسل - فإن 801 بكسل ستنتمي بالفعل إلى الخط الثاني. إذا قمنا بتغييره ، على الشاشة سنرى أن البكسل الأول من الصف الثاني قد تغير (على الرغم من أن العدد في الصفيف سيكون 801 بيكسل). لماذا يوجد 4 عناصر لكل بكسل في المصفوفة؟ هذا لأنه في اللوحة القماشية ، بالإضافة إلى تخصيص عنصر واحد لكل لون - أحمر ، أخضر ، أزرق (هذه 3 عناصر) ، عنصر آخر للشفافية (يقولون أيضًا قناة ألفا أو التعتيم). يتم تعيين قناة ألفا ، مثل اللون ، في النطاق من 0 (شفاف) إلى 255 (معتم). مع هذا الهيكل ، نحصل على صورة 32 بت ،لأن كل بكسل يتكون من 4 عناصر من 8 بتات. لتلخيص: كل بكسل يحتوي على: الأحمر والأخضر والأزرق وقناة ألفا (الشفافية). يسمى نظام الألوان هذا ARGB (Alpha Red Green Blue). وحقيقة أن كل بكسل يشغل 32 بت يشير إلى أن لدينا صورة 32 بت (يقولون أيضًا صورة بعمق لوني 32 بت).

بشكل افتراضي ، المصفوفة الكاملة من وحدات البكسل imageData.data (البيانات هي خاصية تكون فيها مجموعة وحدات البكسل ، و imageData مجرد كائن) مملوءة بالقيم 0 ، وإذا حاولنا إخراج مثل هذا المصفوفة ، فلن نرى أي شيء مثير للاهتمام على الشاشة ، لأن 0 ، 0 ، 0 أسود ، ولكن نظرًا لأن الشفافية هنا ستكون أيضًا 0 ، وهذا لون شفاف تمامًا ، فلن نرى حتى الأسود على الشاشة!

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

class Drawer {
    surface = null
    width = 0
    height = 0

    constructor(surface, width, height) {
        this.surface = surface
        this.width = width
        this.height = height
    }
}

كما ترى في المُنشئ ، تأخذ فئة Drawer جميع البيانات اللازمة وتحفظها. الآن يمكنك إنشاء مثيل من هذه الفئة وتمرير مجموعة من البكسل والعرض والارتفاع فيها (لدينا بالفعل كل هذه البيانات ، لأننا أنشأناها أعلاه وقمنا بتخزينها في imageData):

const drawer = new Drawer(
    imageData.data,
    imageData.width,
    imageData.height
)

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

drawPixel(x, y, r, g, b)  { }

يرجى ملاحظة أن وظيفة drawPixel لا تقبل معلمة ألفا (الشفافية) ، وفوق ذلك اكتشفنا أن مصفوفة البكسل تتكون من 3 معلمات للألوان ومعلمة واحدة للشفافية. لم أقم بتحديد الشفافية على وجه التحديد ، لأننا لا نحتاجها على الإطلاق للحصول على أمثلة. بشكل افتراضي ، سنقوم بتعيين 255 (أي أن كل شيء سيكون معتمًا). الآن دعونا نفكر في كيفية كتابة اللون المطلوب في مجموعة من البكسل في إحداثيات س ، ص. نظرًا لأن لدينا جميع المعلومات حول الصورة يتم تخزينها في صفيف أحادي البعد ، حيث يتم تخصيص 1 بكسل (8 بت) لكل بكسل. للوصول إلى البكسل المطلوب في المصفوفة ، نحتاج أولاً إلى تحديد فهرس الموقع الأحمر ، لأن أي بكسل يبدأ به (على سبيل المثال [r، g، b، a]). شرح صغير لبنية المصفوفة:



يشير الجدول باللون الأخضر إلى كيفية تخزين مكونات الألوان في صفيف سطحي أحادي البعد. يشار إلى مؤشراتهم في نفس الصفيف باللون الأزرق ، وإحداثيات البكسل التي تقبل وظائف drawPixel ، والتي نحتاج إلى تحويلها إلى مؤشرات في الصفيف أحادي البعد ، تشير إلى r ، g ، b ، a للبكسل باللون الأزرق. لذا ، من الجدول ، يمكن ملاحظة أنه بالنسبة لكل بكسل ، يأتي المكون الأحمر للون أولاً ، فلنبدأ به. افترض أننا نريد تغيير المكون الأحمر للون البيكسل في الإحداثيات X1Y1 بحجم صورة 2 × 2 بكسل. في الجدول نرى أن هذا هو المؤشر 12 ، ولكن كيف نحسبه؟ أولاً ، نجد فهرس الصف الذي نحتاجه ، لذلك نقوم بضرب عرض الصورة في Y و 4 (عدد القيم لكل بكسل) - سيكون هذا:

width * y * 4 
//  :
2 * 1 * 4 = 8

نرى أن الخط الثاني يبدأ بالمؤشر 8. إذا قارنا مع اللوحة ، فإن النتيجة تتقارب.

تحتاج الآن إلى إضافة إزاحة عمود إلى فهرس الصف الموجود للحصول على المؤشر الأحمر المطلوب. للقيام بذلك ، أضف X ضرب 4 إلى فهرس الصف. ستكون الصيغة الكاملة:

width * y * 4 + x * 4 
//     :
(width * y + x) * 4
//  :
(2 * 1 + 1) * 4 = 12

نقارن الآن 12 مع الجدول ونرى أن بكسل X1Y1 يبدأ بالفعل بالفهرس 12.

للعثور على مؤشرات مكونات الألوان الأخرى ، تحتاج إلى إضافة إزاحة اللون إلى الفهرس الأحمر: +1 (أخضر) ، +2 (أزرق) ، +3 (ألفا) . يمكننا الآن تنفيذ طريقة drawPixel داخل فئة Drawer باستخدام الصيغة أعلاه:

drawPixel(x, y, r, g, b) {
    const offset = (this.width * y + x) * 4

    this.surface[offset] = r
    this.surface[offset + 1] = g
    this.surface[offset + 2] = b
    this.surface[offset + 3] = 255
}

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

حان الوقت لاختبار الرمز ورؤية البكسل الأول على الشاشة. فيما يلي مثال باستخدام طريقة تجسيد البكسل:

//     Drawer
drawer.drawPixel(10, 10, 255, 0, 0)
drawer.drawPixel(10, 20, 0, 0, 255)

//         canvas
ctx.putImageData(imageData, 0, 0)

في المثال أعلاه ، أرسم 2 بكسل ، واحدة حمراء 255 ، 0 ، 0 ، والأخرى زرقاء 0 ، 0 ، 255. لكن التغييرات في مصفوفة imageData.data (وهي أيضًا السطح داخل فئة درج) لن تظهر على الشاشة. للرسم ، تحتاج إلى استدعاء ctx.putImageData (imageData، 0، 0) ، حيث imageData هي الكائن الذي يكون فيه صفيف وحدات البكسل وعرض / ارتفاع منطقة الرسم ، و 0 ، 0 هي النقطة النسبية التي سيتم عرض صفيف وحدات البكسل فيها (اترك دائمًا 0 ، 0 ) إذا قمت بكل شيء بشكل صحيح ، فستكون لديك الصورة التالية في أعلى يسار عنصر اللوحة القماشية في نافذة المتصفح: هل رأيت



وحدات البكسل؟ إنها صغيرة للغاية ، وكم تم إنجاز العمل.

الآن دعونا نحاول إضافة القليل من الديناميكيات إلى المثال ، على سبيل المثال ، بحيث كل 10 مللي ثانية تتحول وحدات البكسل الخاصة بنا إلى اليمين (نقوم بتغيير X بكسل بمقدار +1 كل 10 مللي ثانية) ، نقوم بتصحيح رمز رسم البكسل بواحد على فاصل زمني:

let x = 10
setInterval(() => {

    drawer.drawPixel(x++, 20, 0, 0, 255)
    ctx.putImageData(imageData, 0, 0)

}, 10)

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



يبقى شريط طويل (أو أثر) لأننا لا نزيل لون البكسل السابق في الصفيف السطحي ، لذلك مع كل استدعاء للفاصل الزمني نضيف بكسل واحد. لنكتب طريقة لتنظيف السطح إلى حالته الأصلية. بمعنى آخر ، املأ الصفيف بالأصفار. أضف طريقة clearSurface إلى فئة Drawer:

clearSurface() {
    const surfaceSize = this.width * this.height * 4
    for (let i = 0; i < surfaceSize; i++) {
        this.surface[i] = 0
    }
}

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

let x = 10
setInterval(() => {
    drawer.clearSurface()
    drawer.drawPixel(x++, 20, 0, 0, 255)
    ctx.putImageData(imageData, 0, 0)
}, 10)

الآن إذا قمت بتشغيل هذا المثال ، فإن البكسل سوف يتحول إلى اليمين ، واحدًا تلو الآخر - بدون أي أثر غير ضروري من الإحداثيات السابقة.

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

drawLine(x1, y1, x2, y2, r, g, b) { }

يتكون أي خط من وحدات بكسل ، ويبقى فقط لملئه بالبكسل بشكل صحيح من x1 ، y1 إلى x2 ، y2. بادئ ذي بدء ، نظرًا لأن الخط يتكون من بكسل ، فإننا سنخرجه بكسلًا تلو الآخر في الحلقة ، ولكن كيف نحسب عدد البكسلات التي يجب إخراجها؟ على سبيل المثال ، لرسم خط من [0 ، 0] إلى [3 ، 0] ، من الواضح بشكل بديهي أنك بحاجة إلى 4 بكسل ([0 ، 0] ، [1 ، 0] ، [2 ، 0] ، [3 ، 0] ،) . ولكن من [12 ، 6] إلى [43 ، 14] ، ليس من الواضح بالفعل طول الخط (عدد البكسل المطلوب عرضه) والإحداثيات التي سيحصلون عليها. للقيام بذلك ، تذكر القليل من الهندسة. لذا ، لدينا خط يبدأ عند x1 و y1 وينتهي عند x2 و y2.


لنرسم خطًا منقطًا من البداية والنهاية حتى نحصل على مثلث (الصورة أعلاه). سنرى أنه عند تقاطع الخطوط المرسومة تكونت زاوية 90 درجة. إذا كان المثلث له مثل هذه الزاوية ، فإن المثلث يسمى مستطيل ، وتسمى أضلاعه ، التي تكون الزاوية 90 درجة بينها ، أرجل. الخط الصلب الثالث (الذي نحاول رسمه) يسمى الوتر في المثلث. باستخدام هذين الساقين المقدّمتين (c1 و c2 في الشكل) ، يمكننا حساب طول الوتر باستخدام نظرية فيثاغورس. دعونا نرى كيف نفعل ذلك. ستكون صيغة طول الوتر (أو طول الخط) كما يلي: 

زوصحولرهنفيسو=إلىورهر12+إلىورهر22


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

 drawLine(x1, y1, x2, y2, r, g, b) {
         const c1 = y2 - y1
         const c2 = x2 - x1

         const length = Math.sqrt(c1 * c1 + c2 * c2)

نحن نعلم بالفعل عدد وحدات البكسل التي يجب رسمها لرسم خط. لكننا لا نعرف حتى الآن كيف يتم تحريك وحدات البكسل. أي أننا نحتاج إلى رسم خط من x1 و y1 إلى x2 و y2 ، ونحن نعلم أن طول الخط سيكون ، على سبيل المثال ، 20 بكسل. يمكننا رسم البكسل الأول في x1 و y1 والأخير في x2 و y2 ، ولكن كيف يمكن العثور على إحداثيات البكسل المتوسطة؟ للقيام بذلك ، نحتاج إلى الحصول على كيفية تحويل كل بكسل التالي فيما يتعلق بـ x1 ، y1 للحصول على الخط المطلوب. سأعطي مثالاً آخر من أجل فهم أفضل لنوع النزوح الذي نتحدث عنه. لدينا نقاط [0 ، 0] و [0 ، 3] ، نحتاج إلى رسم خط عليها. من المثال ، من الواضح أن النقطة التالية بعد [0 ، 0] ستكون [0 ، 1] ، ثم [0 ، 2] وأخيرًا [0 ، 3]. أي أن X من كل نقطة لم يتم إزالتها ، حسنًا ، أو يمكننا القول أنه تم إزالتها بمقدار 0 بكسل ، وتم تحويل Y بمقدار 1 بكسل ، وهذا هو الإزاحة ،يمكن كتابتها كـ [0، 1]. مثال آخر: لدينا نقطة [0 ، 0] ونقطة [3 ، 6] ، دعنا نحاول أن نحسب في أذهاننا كيف تتغير ، الأولى ستكون [0 ، 0] ، ثم [0.5 ، 1] ، ثم [1 ، 2] ثم [1.5 ، 3] وهكذا إلى [3 ، 6] ، في هذا المثال سيكون الإزاحة [0.5 ، 1]. كيف نحسبها؟ 

يمكنك استخدام الصيغة التالية:

   = 2 /  
  Y = 1 /   

في كود البرنامج ، سيكون لدينا هذا:

const xStep = c2 / length
const yStep = c1 / length

جميع البيانات موجودة بالفعل: طول الخط وإزاحة البكسل على طول X و Y. نبدأ في الدورة لرسم:

for (let i = 0; i < length; i++) {
    this.drawPixel(
        Math.trunc(x1 + xStep * i),
        Math.trunc(y1 + yStep * i),
        r, g, b,
    )
}

نظرًا لأن الإحداثيات X لوظيفة Pixel ، فإننا ننقل بداية السطر X + الإزاحة X * i ، وبالتالي نحصل على إحداثيات البكسل i ، نحسب أيضًا إحداثيات Y. Math.trunc هي طريقة في JS تسمح لك بتجاهل الجزء الكسري للرقم. يبدو رمز الطريقة بأكملها كما يلي:

drawLine(x1, y1, x2, y2, r, g, b) {
    const c1 = y2 - y1
    const c2 = x2 - x1

    const length = Math.sqrt(c1 * c1 + c2 * c2)

    const xStep = c2 / length
    const yStep = c1 / length

    for (let i = 0; i < length; i++) {
        this.drawPixel(
            Math.trunc(x1 + xStep * i),
            Math.trunc(y1 + yStep * i),
            r, g, b,
        )
    }
}

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

رمز فئة الدرج
class Drawer {
  surface = null
  width = 0
  height = 0

  constructor(surface, width, height) {
    this.surface = surface
    this.width = width
    this.height = height
  }

  drawPixel(x, y, r, g, b)  {
    const offset = (this.width * y + x) * 4

    this.surface[offset] = r
    this.surface[offset + 1] = g
    this.surface[offset + 2] = b
    this.surface[offset + 3] = 255
  }

  drawLine(x1, y1, x2, y2, r, g, b) {
    const c1 = y2 - y1
    const c2 = x2 - x1

    const length = Math.sqrt(c1 * c1 + c2 * c2)

    const xStep = c2 / length
    const yStep = c1 / length

    for (let i = 0 ; i < length ; i++) {
        this.drawPixel(
          Math.trunc(x1 + xStep * i),
          Math.trunc(y1 + yStep * i),
          r, g, b,
        )
    }
  }

  clearSurface() {
    const surfaceSize = this.width * this.height * 4
    for (let i = 0; i < surfaceSize; i++) {
      this.surface[i] = 0
    }
  }
}

ماذا بعد؟


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

All Articles