دخول
يبدو لي أننا نحتاج إلى استخدام علم حسابي أقل في رسومات الحاسوب. إن الفهم الجيد للإسقاطات والانعكاسات وعمليات المتجهات (كما هو الحال في المعنى الحقيقي للمنتجات المتسلسلة (النقطية) والمتجهات (المتقاطعة) عادةً ما يأتي بإحساس متزايد بالقلق عند استخدام علم المثلثات. بتعبير أدق ، أعتقد أن علم المثلثات مفيد لإدخال البيانات في الخوارزمية (لمفهوم الزوايا ، هذه طريقة بديهية لقياس الاتجاه) ، أشعر أن هناك شيئًا ما خطأ عندما أرى علم المثلثات يقع في أعماق بعض خوارزميات التجسيد ثلاثي الأبعاد. في الواقع ، أعتقد أنه في مكان ما يموت هريرة عندما تزحف مثلثات علم المثلثات هناك. أنا لست قلقًا جدًا من السرعة أو الدقة ، لكن مع الأناقة المفاهيمية ، أعتقد ... الآن سأشرح.
في أماكن أخرى ، سبق أن ناقشت أن المنتجات العددية والمتجهات للناقلات تحتوي على جميع المعلومات اللازمة للدورات ، لهاتين العمليتين "المستطيلة" - الجيب وجيب التمام للزوايا. هذه المعلومات تعادل الجيوب وجيب التمام في عدد كبير من الأماكن التي يبدو أنه يمكنك ببساطة استخدام منتج المتجهات والتخلص من علم المثلثات والزوايا. في الممارسة العملية ، يمكنك القيام بذلك عن طريق البقاء في ناقلات الإقليدية العادية ، دون علم المثلثات على الإطلاق. هذا يجعلنا نتساءل: "ألا نفعل شيئًا لا لزوم له؟" يبدو أن تفعل. ومع ذلك ، لسوء الحظ ، حتى المهنيين ذوي الخبرة هم عرضة لإساءة استخدام علم المثلثات وجعل الأمور معقدة للغاية ومرهقة وليس الأكثر إيجازا. وربما حتى "خطأ".
دعونا نتوقف عن جعل المقال أكثر تجريدًا. دعونا نتخيل إحدى حالات استبدال الصيغ المثلثية بمنتجات المتجهات ونرى ما تحدثت عنه للتو.
خيار خاطئ لتدوير مسافة أو كائن
دعنا نحصل على دالة تقوم بحساب مصفوفة دوران المتجه حول المتجه الطبيعي
في الزاوية
. في أي محرك ثلاثي الأبعاد أو مكتبة للرياضيات في الوقت الفعلي ، ستكون هناك وظيفة من المحتمل أن يتم نسخها بشكل أعمى من محرك آخر ، أو Wikipedia أو OpenGL تعليمي ... (نعم ، في هذه المرحلة ، يجب عليك الاعتراف ، واعتمادًا على حالتك المزاجية ، بسبب هذا).
ستبدو الوظيفة مثل هذا:
mat3x3 rotationAxisAngle( const vec3 & v, float a ) { const float si = sinf( a ); const float co = cosf( a ); const float ic = 1.0f - co; return mat3x3( vx*vx*ic + co, vy*vx*ic - si*vz, vz*vx*ic + si*vy, vx*vy*ic + si*vz, vy*vy*ic + co, vz*vy*ic - si*vx, vx*vz*ic - si*vy, vy*vz*ic + si*vx, vz*vz*ic + co ); }
تخيل أنك تقوم بالبحث من خلال الدواخل التجريبية أو اللعبة ، وربما تنهي نوعًا من وحدة الرسوم المتحركة ، وتحتاج إلى تدوير الكائن في اتجاه معين. تريد تدويرها بحيث يكون أحد محاورها ، على سبيل المثال ، محورًا
تزامن مع ناقل معين
، يقول ، الظل إلى مسار الرسوم المتحركة. أنت بالطبع تقرر إنشاء مصفوفة تحتوي على تحويلات باستخدام
rotationAxisAngle()
. لذلك ، سوف تحتاج أولاً إلى قياس الزاوية بين المحور
الكائن الخاص بك ومتجه الاتجاه المطلوب. نظرًا لأنك مبرمج رسوم ، فأنت تعلم أنه يمكن القيام بذلك باستخدام منتج عددي ثم استخراج الزاوية باستخدام
acos()
.
أيضًا ، أنت تعلم أنه في بعض الأحيان يمكن لـ
acosf()
إرجاع قيم غريبة إذا كان المنتج القياسي خارج النطاق [-1؛ 1] ، وتقرر تغيير قيمتها بحيث تقع في هذا النطاق (
تقريبًا. لكل المشبك) (في هذه المرحلة ، يمكنك حتى أن تجرؤ على إلقاء اللوم على دقة جهاز الكمبيوتر الخاص بك ، لأن طول المتجه المعياري ليس بالضبط 1). عند هذه النقطة ، توفي هريرة واحدة. ولكن حتى تعرف عن ذلك ، يمكنك الاستمرار في كتابة التعليمات البرمجية الخاصة بك. بعد ذلك ، تقوم بحساب محور الدوران ، وأنت تعلم أن هذا المنتج عبارة عن ناقل متجه
الكائن الخاص بك والاتجاه المختار
، سيتم تدوير جميع النقاط في جسمك في طائرات موازية لتلك المحددة في هذين المتجهين ، فقط في حالة ... (تم إحياء الهريرة وقتلها مرة أخرى). نتيجة لذلك ، يبدو الرمز كالتالي:
const vec3 axi = normalize( cross( z, d ) ); const float ang = acosf( clamp( dot( z, d ), -1.0f, 1.0f ) ); const mat3x3 rot = rotationAxisAngle( axi, ang );
لفهم سبب نجاح ذلك ، ولكن لا يزال خطأ ، سنقوم بفتح جميع التعليمات البرمجية
rotationAxisAngle()
ونرى ما يحدث بالفعل:
const vec3 axi = normalize( cross( z, d ) ); const float ang = acosf( clamp( dot( z, d ), -1.0f, 1.0f ) ); const float co = cosf( ang ); const float si = sinf( ang ); const float ic = 1.0f - co; const mat3x3 rot = mat3x3( axi.x*axi.x*ic + co, axi.y*axi.x*ic - si*axi.z, axi.z*axi.x*ic + si*axi.y, axi.x*axi.y*ic + si*axi.z, axi.y*axi.y*ic + co, axi.z*axi.y*ic - si*axi.x, axi.x*axi.z*ic - si*axi.y, axi.y*axi.z*ic + si*axi.x, axi.z*axi.z*ic + co);
كما لاحظت ، نحن نقوم بإجراء مكالمة صوتية غير دقيقة ومكلفة إلى حد ما لإلغائها على الفور عن طريق حساب جيب تمام قيمة الإرجاع. ويظهر السؤال الأول: "لماذا لا يتم تخطي سلسلة
acos()
--->
cos()
وتوفير وقت وحدة المعالجة المركزية؟" علاوة على ذلك ، ألا يخبرنا هذا أننا نقوم بشيء خاطئ ومعقد للغاية ، وأن بعض المبادئ الرياضية البسيطة تأتي إلينا وتتجلى من خلال تبسيط هذا التعبير؟
يمكنك القول بأن التبسيط لا يمكن القيام به ، حيث ستحتاج إلى زاوية لحساب الجيب. ومع ذلك ، هذا ليس كذلك. إذا كنت معتادًا على المنتج المتجه للناقلات ، فأنت تعلم أنه تمامًا مثل المنتج العددي يحتوي على جيب التمام ، يحتوي المتجه على الجيب. يدرك معظم مبرمجي الرسوم سبب الحاجة إلى منتج متجه عددًا من المتجهات ، لكن لا يفهم الجميع سبب الحاجة إلى منتج متجه (ويستخدمونه فقط لقراءة الأمور المعتادة ومحاور التدوير). في الأساس ، يخبرنا المبدأ الرياضي الذي يساعدنا على التخلص من زوج cos / acos أيضًا أنه حيثما يوجد منتج عددي ، فربما يوجد منتج متجه يقوم بالإبلاغ عن جزء المعلومات المفقود (الجزء العمودي ، الجيب).
الطريقة الصحيحة لتدوير مساحة أو كائن
الآن يمكننا استخراج جيب الزاوية بين
و
فقط من خلال النظر إلى طول منتج ناقل ... - تذكر ذلك
و
تطبيع! وهذا يعني أنه يمكننا (يجب علينا !!) إعادة كتابة الوظيفة بهذه الطريقة:
const vec3 axi = cross( z, d ); const float si = length( axi ); const float co = dot( z, d ); const mat3x3 rot = rotationAxisCosSin( axi/si, co, si );
وتأكد من أن وظيفة إنشاء مصفوفة التدوير الجديدة لدينا ،
rotationAxisCosSin()
، لا تقوم بحساب الجيب والجيب التمامي في أي مكان ، ولكنها تأخذها كوسائط:
mat3x3 rotationAxisCosSin( const vec3 & v, const float co, const float si ) { const float ic = 1.0f - co; return mat3x3( vx*vx*ic + co, vy*vx*ic - si*vz, vz*vx*ic + si*vy, vx*vy*ic + si*vz, vy*vy*ic + co, vz*vy*ic - si*vx, vx*vz*ic - si*vy, vy*vz*ic + si*vx, vz*vz*ic + co ); }
هناك شيء آخر يمكن القيام به للتخلص من التطبيع والجذور المربعة - وهو تضمين المنطق بأكمله في وظيفة واحدة جديدة وتمرير
1/si
إلى المصفوفة:
mat3x3 rotationAlign( const vec3 & d, const vec3 & z ) { const vec3 v = cross( z, d ); const float c = dot( z, d ); const float k = (1.0fc)/(1.0fc*c); return mat3x3( vx*vx*k + c, vy*vx*k - vz, vz*vx*k + vy, vx*vy*k + vz, vy*vy*k + c, vz*vy*k - vx, vx*vz*K - vy, vy*vz*k + vx, vz*vz*k + c ); }
في وقت لاحق ، لاحظ Zoltan Vrana أنه يمكن تبسيط
k = 1/(1+c)
إلى
k = 1/(1+c)
، الأمر الذي لا يبدو أكثر أناقة رياضيا فحسب ، ولكنه أيضًا ينقل ميزتين إلى k ، وبالتالي الوظيفة بأكملها (
و
بالتوازي) يذهب إلى واحد (متى
و
يتزامن في هذه الحالة لا يوجد دوران واضح). يبدو الرمز النهائي كالتالي:
mat3x3 rotationAlign( const vec3 & d, const vec3 & z ) { const vec3 v = cross( z, d ); const float c = dot( z, d ); const float k = 1.0f/(1.0f+c); return mat3x3( vx*vx*k + c, vy*vx*k - vz, vz*vx*k + vy, vx*vy*k + vz, vy*vy*k + c, vz*vy*k - vx, vx*vz*K - vy, vy*vz*k + vx, vz*vz*k + c ); }
نحن لا نتخلص فقط من ثلاث وظائف مثلثية ونتخلص من المشبك القبيح (والتطبيع!) ، ولكننا أيضًا قمنا بتبسيط الرياضيات ثلاثية الأبعاد. لا توجد وظائف متعالية ، حيث يتم استخدام المتجهات فقط هنا. المتجهات خلق المصفوفات التي تعدل ناقلات أخرى. وهذا أمر مهم ، نظرًا لأن علم المثلثات الأقل في محركك ثلاثي الأبعاد ، ليس فقط أسرع وأكثر وضوحًا ، ولكن أولاً وقبل كل شيء ، أكثر أناقة من الناحية الرياضية (أكثر صحة!).