Évitez la trigonométrie

Entrée


Il me semble que nous devons utiliser moins de trigonométrie en infographie. Une bonne compréhension des projections, des réflexions et des opérations vectorielles (comme dans le vrai sens des produits scalaires (point) et vectoriels (croisés) des vecteurs) s'accompagne généralement d'un sentiment croissant d'anxiété lors de l'utilisation de la trigonométrie. Plus précisément, je pense que la trigonométrie est bonne pour entrer des données dans l'algorithme (pour le concept d'angles, c'est une façon intuitive de mesurer l'orientation), je pense que quelque chose ne va pas quand je vois la trigonométrie située dans les profondeurs d'un algorithme de rendu 3D. En fait, je pense que quelque part un chaton meurt lorsque la trigonométrie s'y glisse. Et je ne m'inquiète pas tant de la vitesse ou de la précision, mais avec une élégance conceptuelle, je pense ... Maintenant je vais vous expliquer.

Dans d'autres endroits, j'ai déjà expliqué que les produits scalaires et vectoriels des vecteurs contiennent toutes les informations nécessaires aux rotations, pour ces deux opérations «rectangulaires» - les sinus et les cosinus des angles. Ces informations sont équivalentes aux sinus et cosinus dans un si grand nombre d'endroits qu'il semble que vous pouvez simplement utiliser le produit des vecteurs et vous débarrasser de la trigonométrie et des angles. En pratique, vous pouvez le faire en restant dans des vecteurs euclidiens ordinaires, sans aucune trigonométrie. Cela vous fait vous demander: "Ne faisons-nous pas quelque chose de plus?" Semble faire. Cependant, malheureusement, même les professionnels expérimentés sont enclins à abuser de la trigonométrie et à rendre les choses très complexes, lourdes et pas les plus concises. Et peut-être même "faux".

Arrêtons de rendre l'article encore plus abstrait. Imaginons un des cas de remplacement des formules trigonométriques par des produits vectoriels et voyons de quoi je viens de parler.

Mauvaise option pour faire pivoter un espace ou un objet


Ayons une fonction qui calcule la matrice de rotation d'un vecteur autour d'un vecteur normalisé  vecvau coin a. Dans n'importe quel moteur 3D ou bibliothèque mathématique en temps réel, il y aura une de ces fonctions, qui est très probablement copiée à l'aveugle à partir d'un autre moteur, d'un tutoriel Wikipedia ou OpenGL ... à cause de cela).

La fonction ressemblera à ceci:

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 ); } 

Imaginez que vous fouillez à l'intérieur d'une démo ou d'un jeu, en terminant éventuellement une sorte de module d'animation, et que vous devez faire pivoter l'objet dans une direction donnée. Vous voulez le faire pivoter de sorte que l'un de ses axes, disons, un axe  veczcoïncidé avec un vecteur spécifique  vecd, par exemple, tangente à la trajectoire d'animation. Bien sûr, vous décidez de créer une matrice qui contiendra des transformations à l'aide de rotationAxisAngle() . Donc, vous devrez d'abord mesurer l'angle entre l'axe zvotre objet et le vecteur d'orientation souhaité. Puisque vous êtes un programmeur graphique, vous savez que cela peut être fait avec un produit scalaire et ensuite extraire l'angle avec acos() .

 veca cdot vecb=axbx+ayby=ab cos angle veca vecb

En outre, vous savez que parfois acosf() peut renvoyer des valeurs étranges si le produit scalaire est en dehors de la plage [-1; 1], et vous décidez de modifier sa valeur afin qu'elle tombe dans cette plage ( environ Per. Pour bloquer) (à ce stade, vous pouvez même oser blâmer la précision de votre ordinateur, car la longueur du vecteur normalisé n'est pas exactement 1). À ce stade, un chaton est décédé. Mais jusqu'à ce que vous le sachiez, vous continuez à écrire votre code. Ensuite, vous calculez l'axe de rotation, et vous savez qu'il s'agit d'un produit vectoriel d'un vecteur  veczvotre objet et la direction choisie  vecd, tous les points de votre objet tourneront dans des plans parallèles à celui défini par ces deux vecteurs, juste au cas où ... (le chaton a été ravivé et tué à nouveau). Par conséquent, le code ressemble à ceci:

 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 ); 

Pour comprendre pourquoi cela fonctionne, mais toujours par erreur, nous allons ouvrir tout le code rotationAxisAngle() et voir ce qui se passe vraiment:

 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); 

Comme vous l'avez peut-être remarqué, nous effectuons un appel acos assez inexact et coûteux pour l'annuler immédiatement en calculant le cosinus de la valeur de retour. Et la première question apparaît: "pourquoi ne pas sauter la chaîne d'appel acos() ---> cos() et gagner du temps CPU?" De plus, cela ne nous dit-il pas que nous faisons quelque chose de mal et de très compliqué, et que nous vient un principe mathématique simple qui se manifeste par la simplification de cette expression?

Vous pouvez affirmer que la simplification ne peut pas être effectuée, car vous aurez besoin d'un angle pour calculer le sinus. Mais ce n'est pas le cas. Si vous connaissez le produit vectoriel des vecteurs, vous savez que, tout comme le produit scalaire contient le cosinus, le vecteur contient le sinus. La plupart des programmeurs graphiques comprennent pourquoi un produit scalaire de vecteurs est nécessaire, mais tout le monde ne comprend pas pourquoi un produit vectoriel est nécessaire (et ne l'utilise que pour lire des normales et des axes de rotation). Fondamentalement, le principe mathématique qui nous aide à nous débarrasser de la paire cos / acos nous dit également que là où il y a un produit scalaire, il y a peut-être un produit vectoriel qui rapporte l'information manquante (partie perpendiculaire, sinus).

|| veca times vecb||=ab sin angle veca vecb

La bonne façon de faire pivoter un espace ou un objet


Maintenant, nous pouvons extraire le sinus de l'angle entre  veczet  vecdjuste en regardant la longueur de leur produit vectoriel ... - rappelez-vous que  veczet  vecdnormalisé! Et cela signifie que nous pouvons (nous devons !!) réécrire la fonction de cette façon:

 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 ); 

et assurez-vous que notre nouvelle fonction de construction de matrice de rotation, rotationAxisCosSin() , ne calcule aucun sinus et cosinus, mais les prend comme arguments:

 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 ); } 

Il y a encore une chose à faire pour se débarrasser des normalisations et des racines carrées - encapsuler toute la logique dans une nouvelle fonction et passer 1/si à la matrice:

 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 ); } 

Plus tard, Zoltan Vrana a remarqué que k peut être simplifié en k = 1/(1+c) , ce qui non seulement semble mathématiquement plus élégant, mais déplace également deux caractéristiques vers k et, ainsi, l'ensemble de la fonction (  vecdet  veczparallèle) va en un (lorsque  vecdet  veczcoïncident dans ce cas il n'y a pas de rotation nette). Le code final ressemble à ceci:

 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 ); } 

Non seulement nous nous sommes débarrassés de trois fonctions trigonométriques et nous nous sommes débarrassés de la pince laide (et de la normalisation!), Mais nous avons également simplifié conceptuellement nos mathématiques 3D. Pas de fonctions transcendantales, seuls les vecteurs sont utilisés ici. Les vecteurs créent des matrices qui modifient d'autres vecteurs. Et cela est important, car moins il y a de trigonométrie dans votre moteur 3D, plus il devient rapide et clair, mais, d'abord, mathématiquement plus élégant (plus correct!).

Source: https://habr.com/ru/post/fr462539/


All Articles