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

اسمحوا لي أن أدخل صورة متحركة (ستة أمتار):

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

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

انظر الكود
هنا ، لكن دعني أقدم لك الشفرة مباشرة في نص المقال:
#define _USE_MATH_DEFINES #include <cmath> #include <algorithm> #include <limits> #include <iostream> #include <fstream> #include <vector> #include "geometry.h" const float sphere_radius = 1.5; float signed_distance(const Vec3f &p) { return p.norm() - sphere_radius; } bool sphere_trace(const Vec3f &orig, const Vec3f &dir, Vec3f &pos) { pos = orig; for (size_t i=0; i<128; i++) { float d = signed_distance(pos); if (d < 0) return true; pos = pos + dir*std::max(d*0.1f, .01f); } return false; } int main() { const int width = 640; const int height = 480; const float fov = M_PI/3.; std::vector<Vec3f> framebuffer(width*height); #pragma omp parallel for for (size_t j = 0; j<height; j++) { // actual rendering loop for (size_t i = 0; i<width; i++) { float dir_x = (i + 0.5) - width/2.; float dir_y = -(j + 0.5) + height/2.; // this flips the image at the same time float dir_z = -height/(2.*tan(fov/2.)); Vec3f hit; if (sphere_trace(Vec3f(0, 0, 3), Vec3f(dir_x, dir_y, dir_z).normalize(), hit)) { // the camera is placed to (0,0,3) and it looks along the -z axis framebuffer[i+j*width] = Vec3f(1, 1, 1); } else { framebuffer[i+j*width] = Vec3f(0.2, 0.7, 0.8); // background color } } } std::ofstream ofs("./out.ppm", std::ios::binary); // save the framebuffer to file ofs << "P6\n" << width << " " << height << "\n255\n"; for (size_t i = 0; i < height*width; ++i) { for (size_t j = 0; j<3; j++) { ofs << (char)(std::max(0, std::min(255, static_cast<int>(255*framebuffer[i][j])))); } } ofs.close(); return 0; }
تعيش فئة المتجهات في ملف geometry.h ، ولن أصفها هنا: أولاً ، كل شيء بسيط ، والتعامل البسيط مع المتجهات ثنائية وثلاثية الأبعاد (إضافة وطرح وتعيين وضرب بمنتج عددي أو منتج عددي) وثانياً
gbg سبق
وصفها بالتفصيل كجزء من دورة محاضرة حول رسومات الحاسوب.
أحفظ الصورة
بتنسيق ppm ؛ هذه هي أسهل طريقة لحفظ الصور ، رغم أنها ليست الأكثر ملاءمة دائمًا للعرض الإضافي.
لذلك ، في الوظيفة الرئيسية () ، لدي دورتان: الدورة الثانية تحفظ ببساطة الصورة على القرص ، والدورة الأولى تمر عبر جميع بكسلات الصورة ، وتصدر شعاعًا من الكاميرا عبر هذا البيكسل ، وتتطلع إلى معرفة ما إذا كان هذا الشعاع يتقاطع مع منطقتنا.
انتبه ، الفكرة الرئيسية للمقال: إذا نظرنا في المقال الأخير بشكل تحليلي إلى تقاطع الشعاع والمجال ، فأنا الآن أحسبه عدديًا. الفكرة بسيطة: تحتوي الكرة على معادلة من الشكل x ^ 2 + y ^ 2 + z ^ 2 - r ^ 2 = 0؛ ولكن بشكل عام ، يتم تعريف الدالة f (x، y، z) = x ^ 2 + y ^ 2 + z ^ 2 - r ^ 2 في المساحة بالكامل. داخل المجال ، سيكون للوظيفة f (x ، y ، z) قيم سالبة ، وخارج المجال ، ستكون موجبة. بمعنى ، تقوم الدالة f (x ، y ، z) بتعيين المسافة (بعلامة!) إلى مجالنا الخاص بالنقطة (x ، y ، z). لذلك ، نحن ببساطة ننزلق على طول الشعاع حتى نحس بالملل أو تصبح الوظيفة f (x ، y ، z) سالبة. وظيفة sphere_trace () تفعل ذلك بالضبط.
المرحلة الثالثة: الإضاءة البدائية
لنرمز إلى أبسط إضاءة منتشرة ، أريد الحصول على مثل هذه الصورة في الإخراج:

كما في المقالة السابقة ، لسهولة القراءة ، فعلت خطوة واحدة = التزام واحد. يمكن
رؤية التغييرات
هنا .
بالنسبة للإضاءة المنتشرة ، لا يكفي أن نحسب نقطة تقاطع الحزمة مع السطح ، فنحن بحاجة إلى معرفة المتجه الطبيعي إلى السطح في هذه المرحلة. استلمت هذا المتجه الطبيعي من خلال
اختلافات بسيطة بسيطة في
وظيفتنا للمسافة إلى السطح:
Vec3f distance_field_normal(const Vec3f &pos) { const float eps = 0.1; float d = signed_distance(pos); float nx = signed_distance(pos + Vec3f(eps, 0, 0)) - d; float ny = signed_distance(pos + Vec3f(0, eps, 0)) - d; float nz = signed_distance(pos + Vec3f(0, 0, eps)) - d; return Vec3f(nx, ny, nz).normalize(); }
من حيث المبدأ ، بالطبع ، بما أننا نرسم كرة ، يمكن الحصول على الوضع الطبيعي بسهولة أكبر ، لكنني فعلت ذلك باحتياطي للمستقبل.
المرحلة الرابعة: لنرسم نمطًا على مجالنا
ودعنا نرسم بعض الأنماط في منطقتنا ، على سبيل المثال ، مثل هذا:

للقيام بذلك ، في الرمز السابق ، لقد غيرت
سطرين فقط!كيف فعلت هذا؟ بالطبع ، ليس لدي أي قوام. لقد أخذت الدالة g (x، y، z) = sin (x) * sin (y) * sin (z)؛ يتم تعريفها مرة أخرى في جميع أنحاء الفضاء. عندما يعبر شعاعي الكرة في نقطة ما ، فإن قيمة الدالة g (x ، y ، z) في هذه المرحلة تحدد لون البيكسل بالنسبة لي.
بالمناسبة ، انتبه إلى دوائر متحدة المركز حول الكرة - فهذه عبارة عن قطع أثرية لحسابي العددي للتقاطع.
الخطوة الخامسة: رسم خرائط النزوح
لماذا أردت رسم هذا النمط؟ وسوف يساعدني في رسم مثل هذه القنفذ:

عندما يكون نمطي أسود ، أريد أن أضغط في محيطنا ، وحيث كان أبيض ، على العكس من ذلك ، قم بتمديد الحدبة.
للقيام بذلك ، ما عليك سوى
تغيير الأسطر الثلاثة في الكود:
float signed_distance(const Vec3f &p) { Vec3f s = Vec3f(p).normalize(sphere_radius); float displacement = sin(16*sx)*sin(16*sy)*sin(16*sz)*noise_amplitude; return p.norm() - (sphere_radius + displacement); }
بمعنى ، لقد قمت بتغيير حساب المسافة إلى سطحنا ، مع تعريفها على أنها x ^ 2 + y ^ 2 + z ^ 2 - r ^ 2 - sin (x) * sin (y) * sin (z). في الواقع ، حددنا
وظيفة ضمنية .
الخطوة السادسة: وظيفة أخرى ضمنية
ولماذا أقوم بتقييم منتج الجيب فقط للحصول على نقاط ملقاة على سطح منطقتنا؟ دعنا نعيد تعريف وظيفتنا الضمنية مثل هذا:
float signed_distance(const Vec3f &p) { float displacement = sin(16*px)*sin(16*py)*sin(16*pz)*noise_amplitude; return p.norm() - (sphere_radius + displacement); }
الفرق مع الكود السابق صغير جدًا ، فمن الأفضل
رؤية الفرق . هذه هي النتيجة:

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

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

ثم فقط لخص لهم:

اقرأ المزيد
هنا وهنا .
دعونا
نضيف بعض الكود الذي يولد هذا الضجيج والحصول على هذه الصورة:

يرجى ملاحظة أنه في رمز العرض لم أغير أي شيء على الإطلاق ، فقط الوظيفة التي "تتجعد" قد تغيرت مجالنا.
المرحلة الثامنة ، النهائية: إضافة اللون
الشيء الوحيد الذي
غيرته في هذا الالتزام هو أنه بدلاً من اللون الأبيض الموحد ، قمت بتطبيق لون يعتمد خطيًا على مقدار الضوضاء المطبقة:
Vec3f palette_fire(const float d) { const Vec3f yellow(1.7, 1.3, 1.0);
هذا تدرج خطي بسيط بين خمسة ألوان رئيسية. حسنا ، ها هي الصورة!

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