[الجزءان
الأول والثاني .]
اليوم سنحقق قفزة كبيرة. سنبتعد عن الهياكل الكروية الحصرية والطائرة اللانهائية التي تتبعناها في وقت سابق ، ونضيف مثلثات - الجوهر الكامل لرسومات الكمبيوتر الحديثة ، وهو عنصر تتألف منه جميع العوالم الافتراضية. إذا كنت تريد المتابعة مع ما انتهينا من آخر مرة ، فاستخدم
الرمز من الجزء 2 . الكود النهائي لما سنقوم به اليوم متاح
هنا . لنبدأ!
مثلثات
المثلث هو مجرد قائمة بثلاثة
رؤوس متصلة ، يخزن كل منها موقعه الخاص ، وأحيانًا طبيعي. يحدد ترتيب اجتياز القمم من وجهة نظرك ما الذي ننظر إليه - الجانب الأمامي أو الخلفي للمثلث. تقليديًا ، تُعتبر "الجبهة" هي ترتيب اجتياز عقارب الساعة.
أولاً ، يجب أن نكون قادرين على تحديد ما إذا كان الشعاع يتقاطع مع مثلث ، وإذا كان الأمر كذلك ، في أي نقطة. في عام 1997 ، اقترح السادة
توماس أكينين ميلر وبن ترمبور خوارزمية شائعة للغاية (ولكن بالتأكيد
ليست الوحيدة ) لتحديد تقاطع الشعاع مع مثلث. يمكنك قراءة المزيد حول هذا الموضوع في مقالهم "تقاطع مثلث شعاع التخزين السريع ، الحد الأدنى"
هنا .
يمكن نقل الرمز من المقال بسهولة إلى رمز تظليل HLSL:
static const float EPSILON = 1e-8; bool IntersectTriangle_MT97(Ray ray, float3 vert0, float3 vert1, float3 vert2, inout float t, inout float u, inout float v) {
لاستخدام هذه الوظيفة ، نحتاج إلى شعاع وثلاثة رؤوس للمثلث. تخبرنا قيمة الإرجاع ما إذا كان المثلث يتقاطع. في حالة التقاطع ، تُحسب ثلاث قيم إضافية:
t
تصف المسافة على طول الحزمة إلى نقطة التقاطع ، و
u
/
v
هما من الإحداثيات الثلاثة ثنائية المركز التي تحدد موقع نقطة التقاطع على المثلث (يمكن حساب الإحداثي الأخير كـ
w = 1 - u - v
). إذا لم تكن معتادًا على
الإحداثيات ثنائية المركز حتى الآن ، فاقرأ
شرحهم الممتاز على
Scratchapixel .
دون تأخير كبير ، دعنا نتتبع مثلثًا واحدًا مع الرؤوس الموضحة في الكود! ابحث عن وظيفة
Trace
في التظليل وأضف جزء الشفرة التالي إليها:
كما قلت ، يخزن المسافة على طول الحزمة ، ويمكننا استخدام هذه القيمة مباشرة لحساب نقطة التقاطع. يمكن حساب المعدل الطبيعي ، وهو أمر مهم لحساب الانعكاس الصحيح ، باستخدام المنتج الموجه لأي حافتي المثلث. شغّل وضع اللعبة واستمتع بمثلث تتبعك الأول:
التمرين: حاول حساب الموضع باستخدام إحداثيات متحدة المركز بدلاً من المسافة. إذا قمت بكل شيء بشكل صحيح ، فسيبدو المثلث اللامع تمامًا من قبل.
مثلث تنسجم
لقد تغلبنا على العقبة الأولى ، ولكن تتبع الشبكات الكاملة من المثلثات هو قصة مختلفة تمامًا. نحتاج أولاً إلى تعلم بعض المعلومات الأساسية حول الشبكات. إذا كنت تعرفهم ، فيمكنك تخطي الفقرة التالية بأمان.
في رسومات الكمبيوتر ، يتم تعريف الشبكة من خلال العديد من المخازن المؤقتة ، وأهمها المخازن المؤقتة
الرأسية والفهرس .
المخزن المؤقت لـ vertex عبارة عن قائمة من المتجهات ثلاثية الأبعاد التي تصف موضع كل قمة في
مساحة الكائن (وهذا يعني أن هذه القيم لا تحتاج إلى تغيير عند نقل كائن أو تدويره أو تغيير حجمه - يتم تحويلها من
مساحة الكائن إلى
مساحة العالم أثناء الطيران باستخدام مضاعفة المصفوفة) .
المخزن المؤقت الفهرس هو قائمة قيم عدد صحيح التي هي
الفهارس التي تشير إلى المخزن المؤقت قمة الرأس. كل ثلاثة فهارس تشكل مثلث. على سبيل المثال ، إذا كان المخزن المؤقت الفهرس له النموذج [0 ، 1 ، 2 ، 0 ، 2 ، 3] ، عنده مثلثان: يتكون المثلث الأول من القمم الأولى والثانية والثالثة في المخزن المؤقت لـ vertex ، ويتكون المثلث الثاني من الأول والثاني والقمم الرابعة. لذلك ، يحدد المخزن المؤقت الفهرس أيضًا ترتيب traversal المذكور أعلاه. بالإضافة إلى المخازن المؤقتة والفهارس قمة الرأس ، قد يكون هناك مخازن مؤقتة إضافية تضيف معلومات أخرى إلى كل قمة. تقوم المخازن المؤقتة الإضافية الأكثر شيوعًا بتخزين
الأشياء الطبيعية وتنسيقات النسيج (تسمى
texcoords أو
UV ببساطة) ، وكذلك
ألوان قمة الرأس .
باستخدام GameObjects
بادئ ذي بدء ، نحن بحاجة إلى معرفة أي من GameObjects يجب أن يصبح جزءًا من عملية تتبع الأشعة. الحل الساذج هو ببساطة استخدام
FindObjectOfType<MeshRenderer>()
، ولكن القيام بشيء أكثر مرونة وأسرع. لنقم بإضافة مكون
RayTracingObject
جديد:
using UnityEngine; [RequireComponent(typeof(MeshRenderer))] [RequireComponent(typeof(MeshFilter))] public class RayTracingObject : MonoBehaviour { private void OnEnable() { RayTracingMaster.RegisterObject(this); } private void OnDisable() { RayTracingMaster.UnregisterObject(this); } }
تتم إضافة هذا المكون إلى كل كائن نريد استخدامه لتتبع الأشعة ويشارك في تسجيله باستخدام
RayTracingMaster
. أضف الوظائف التالية إلى المعالج:
private static bool _meshObjectsNeedRebuilding = false; private static List<RayTracingObject> _rayTracingObjects = new List<RayTracingObject>(); public static void RegisterObject(RayTracingObject obj) { _rayTracingObjects.Add(obj); _meshObjectsNeedRebuilding = true; } public static void UnregisterObject(RayTracingObject obj) { _rayTracingObjects.Remove(obj); _meshObjectsNeedRebuilding = true; }
كل شيء يسير على ما يرام - الآن نعرف الأشياء التي يجب تتبعها. ولكن بعد ذلك يأتي الجزء الصعب: سنقوم بجمع جميع البيانات من شبكات الوحدة (المصفوفة ، ومخازن القمة الرأسية والفهارس - تذكرها؟) ، وكتابتها على هياكل البيانات الخاصة بنا وتحميلها في GPU بحيث يمكن للتظليل استخدامها. لنبدأ بتحديد بنيات البيانات والمخازن المؤقتة على الجانب C # ، في المعالج:
struct MeshObject { public Matrix4x4 localToWorldMatrix; public int indices_offset; public int indices_count; } private static List<MeshObject> _meshObjects = new List<MeshObject>(); private static List<Vector3> _vertices = new List<Vector3>(); private static List<int> _indices = new List<int>(); private ComputeBuffer _meshObjectBuffer; private ComputeBuffer _vertexBuffer; private ComputeBuffer _indexBuffer;
... والآن دعونا نفعل الشيء نفسه في التظليل. هل اعتدت على ذلك؟
struct MeshObject { float4x4 localToWorldMatrix; int indices_offset; int indices_count; }; StructuredBuffer<MeshObject> _MeshObjects; StructuredBuffer<float3> _Vertices; StructuredBuffer<int> _Indices;
هياكل البيانات جاهزة ، ويمكننا تعبئتها ببيانات حقيقية. نحن نجمع كل رؤوس كل الشبكات في
List<Vector3>
كبيرة واحدة
List<Vector3>
، وجميع الفهارس في
List<int>
كبيرة
List<int>
. لا توجد مشاكل مع القمم ، ولكن يجب تغيير المؤشرات بحيث تستمر في الإشارة إلى القمة الصحيحة في المخزن المؤقت الكبير الخاص بنا. تخيل أننا قمنا بالفعل بإضافة كائنات من 1000 رأس ، والآن نضيف مكعبًا شبكيًا بسيطًا. قد يتكون المثلث الأول من مؤشرات [0 ، 1 ، 2] ، ولكن نظرًا لأن لدينا بالفعل 1000 رأس في المخزن المؤقت ، نحتاج إلى تبديل المؤشرات قبل إضافة رؤوس إلى المكعب. أي أنها ستتحول إلى [1000 ، 1001 ، 1002]. إليك ما يبدو في الكود:
private void RebuildMeshObjectBuffers() { if (!_meshObjectsNeedRebuilding) { return; } _meshObjectsNeedRebuilding = false; _currentSample = 0;
نحن ندعو
RebuildMeshObjectBuffers
في وظيفة
OnRenderImage
، ولا تنس تحرير المخازن المؤقتة الجديدة في
OnDisable
. فيما يلي وظيفتان مساعدتان استخدمتهما في الكود أعلاه لتبسيط معالجة المخزن المؤقت قليلاً:
private static void CreateComputeBuffer<T>(ref ComputeBuffer buffer, List<T> data, int stride) where T : struct {
عظيم ، أنشأنا المخازن المؤقتة وأنها مليئة بالبيانات اللازمة! الآن نحن بحاجة فقط إلى الإبلاغ عن هذا إلى تظليل. أضف التعليمات البرمجية التالية إلى
SetShaderParameters
(وبفضل وظائف المساعد الجديدة ، يمكننا تقليل رمز المخزن المؤقت للكرة):
SetComputeBuffer("_Spheres", _sphereBuffer); SetComputeBuffer("_MeshObjects", _meshObjectBuffer); SetComputeBuffer("_Vertices", _vertexBuffer); SetComputeBuffer("_Indices", _indexBuffer);
لذا ، فإن العمل ممل ، ولكن دعونا نرى ما فعلناه للتو: لقد جمعنا جميع البيانات الداخلية للشبكات (المصفوفة والرؤوس والفهارس) ، ووضعناها في بنية مريحة وبسيطة ، ثم أرسلناها إلى وحدة معالجة الرسومات ، والتي تتطلع الآن إلى متى يمكن استخدامها.
تتبع شبكة
دعونا لا نجعله ينتظر. في التظليل ، لدينا بالفعل شفرة التتبع لمثلث فردي ، والشبكة هي في الواقع مجرد الكثير من المثلثات. الجانب الجديد الوحيد هنا هو أننا نستخدم المصفوفة لتحويل الرؤوس من مساحة الكائن إلى مساحة العالم باستخدام دالة
mul
المدمجة (اختصار للضرب). تحتوي المصفوفة على ترجمة الكائن وتناوبه وحجمه. يبلغ حجمها 4 × 4 ، لذلك بالنسبة للضرب ، نحتاج إلى ناقل 4d. يتم أخذ المكونات الثلاثة الأولى (س ، ص ، ض) من المخزن المؤقت قمة الرأس. قمنا بتعيين المكون الرابع (ث) إلى 1 لأننا نتعامل مع نقطة. إذا كان هذا هو الاتجاه ، فسنكتب 0 فيه لتجاهل كل الترجمات والمقياس في المصفوفة. هل هذا مربك بالنسبة لك؟ ثم اقرأ
هذا البرنامج التعليمي ثماني مرات على الأقل. هنا هو رمز التظليل:
void IntersectMeshObject(Ray ray, inout RayHit bestHit, MeshObject meshObject) { uint offset = meshObject.indices_offset; uint count = offset + meshObject.indices_count; for (uint i = offset; i < count; i += 3) { float3 v0 = (mul(meshObject.localToWorldMatrix, float4(_Vertices[_Indices[i]], 1))).xyz; float3 v1 = (mul(meshObject.localToWorldMatrix, float4(_Vertices[_Indices[i + 1]], 1))).xyz; float3 v2 = (mul(meshObject.localToWorldMatrix, float4(_Vertices[_Indices[i + 2]], 1))).xyz; float t, u, v; if (IntersectTriangle_MT97(ray, v0, v1, v2, t, u, v)) { if (t > 0 && t < bestHit.distance) { bestHit.distance = t; bestHit.position = ray.origin + t * ray.direction; bestHit.normal = normalize(cross(v1 - v0, v2 - v0)); bestHit.albedo = 0.0f; bestHit.specular = 0.65f; bestHit.smoothness = 0.99f; bestHit.emission = 0.0f; } } } }
نحن على بعد خطوة واحدة فقط من رؤيته في العمل. دعنا نعيد هيكلة وظيفة
Trace
قليلاً ونضيف تتبعًا لكائنات الشبكة:
RayHit Trace(Ray ray) { RayHit bestHit = CreateRayHit(); uint count, stride, i;
النتائج
هذا كل شئ! دعنا نضيف بعض الشبكات البسيطة (
RayTracingObject
الوحدة جيدة) ، ومنحهم مكون
RayTracingObject
ومراقبة السحر.
لا تستخدم شبكات مفصلة حتى الآن (أكثر من بضع مئات من المثلثات)! يفتقر تظليلنا إلى التحسين ، وإذا تجاوزته ، فقد يستغرق الأمر ثوانٍ أو حتى دقائق لتتبع عينة واحدة على الأقل لكل بكسل. نتيجة لذلك ، سوف يقوم النظام بإيقاف برنامج التشغيل GPU ، وقد يتعطل محرك Unity ، وسيحتاج الكمبيوتر إلى إعادة التشغيل.
لاحظ أن تنسجم لدينا لا تملك التظليل السلس ، ولكن مسطحة. نظرًا لأننا لم نحمل بعد القواعد الطبيعية للرؤوس في المخزن المؤقت ، للحصول على القمم العادية لكل مثلث ، نحتاج إلى إجراء منتج متجه. بالإضافة إلى ذلك ، لا يمكننا التحريف فوق منطقة المثلث. سنتعامل مع هذه المشكلة في الجزء التالي من البرنامج التعليمي.
من أجل الاهتمام ، قمت بتنزيل Stanford Bunny من
أرشيف Morgan McGwire واستخدام المعدِّل
العشري لحزمة
Blender I خفضت عدد الرؤوس إلى 431. يمكنك تجربة معلمات الإضاءة والمواد ذات الترميز الثابت في وظيفة تظليل
IntersectMeshObject
. إليك أرنب عازل بظلال ناعمة جميلة وإضاءة عالمية منتشرة قليلاً في
Grafitti Shelter :
... وإليك أرنب معدني تحت ضوء الاتجاه القوي
لكيب هيل ، يلقي بريق الديسكو على مستوى الأرض:
... وهنا اثنين من الأرانب الصغيرة يختبئون تحت حجر كبير سوزان تحت السماء الزرقاء
Kiara 9 Dusk (وصفت مادة بديلة للكائن الثاني ، والتحقق مما إذا كان مؤشر الفهرس هو الصفر):
ما التالي؟
من الرائع رؤية شبكة حقيقية في التتبع الخاص بك للمرة الأولى ، أليس كذلك؟ اليوم ، قمنا بمعالجة بعض البيانات ، واكتشفنا التقاطع باستخدام خوارزمية Meller-Trambor ، وجمعنا كل شيء حتى نتمكن من استخدام محرك GameObjects لمحرك Unity على الفور. بالإضافة إلى ذلك ، رأينا إحدى مزايا تتبع الأشعة: بمجرد إضافة تقاطع جديد إلى الكود ، تبدأ جميع التأثيرات الجميلة (الظلال الناعمة والإضاءة العالمية المنعكسة والمنتشرة وما إلى ذلك) على الفور في العمل.
استغرق تقديم الأرنب اللامع الكثير من الوقت ، وما زلت مضطرًا لاستخدام القليل من الترشيح للتخلص من الضوضاء الأكثر وضوحًا. لحل هذه المشكلة ، عادةً ما يتم كتابة مشهد في بنية مكانية ، على سبيل المثال ، في شبكة ، شجرة K- الأبعاد أو تسلسل هرمي من وحدات التخزين المحيط ، مما يزيد بشكل كبير من سرعة عرض المشاهد الكبيرة.
لكننا نحتاج إلى التحرك بالترتيب: علاوة على ذلك ، سنقوم بإزالة المشكلة المتعلقة بالألوان الطبيعية حتى نبدو تنسجم (حتى المنخفضة الكثافة) أكثر نعومة من الآن. سيكون من الجيد أيضًا تحديث المصفوفات تلقائيًا عند نقل الكائنات والإشارة مباشرةً إلى مواد الوحدة ، وليس فقط كتابتها في الكود. هذا هو ما سنفعله في الجزء التالي من سلسلة البرنامج التعليمي. شكرا للقراءة ، وأراك في الجزء 4!