2.9.4 Bounding moving bounding boxes
Donné un Bounds3f qui est transformé par une transformation animée, il est utile de pouvoir calculer une bounding box qui englobe tout son mouvement sur la période de temps de l’animation. Par exemple, si nous pouvons délimiter le mouvement d’une primitive géométrique animée, nous pouvons intersecter des rayons avec cette délimitation pour déterminer si le rayon peut couper l’objet avant d’encourir le coût de l’interpolation de la délimitation de la primitive au temps du rayon pour vérifier cette intersection. La méthode AnimatedTransform::MotionBounds() effectue ce calcul, prenant une boîte englobante et renvoyant la boîte englobante de son mouvement sur la plage de temps de l’AnimatedTransform.
AnimatedTransform 103
AnimatedTransform: : Interpolate() 106
AnimatedTransform: : MotionBounds() 108
AnimatedTransform::R 103
AnimatedTransform: :S 103
Float 1062
Matrix4x4 1081
Point3f 68
Quaternion 99
Quaternion::ToTransform() 101
Ray 73
Ray: :time 73
RayDifférentiel 75
Slerp() 103
Transformation 83
Translate() 87
Vector3f 60
Il y a deux cas faciles : premièrement, si les matrices d’images clés sont égales, alors nous pouvons arbitrairement appliquer seulement la transformation de départ pour calculer les limites complètes. Deuxièmement, si la transformation ne comprend qu’une mise à l’échelle et/ou une translation, alors la boîte englobant les positions transformées de la boîte englobante à l’heure de début et à l’heure de fin délimite tous ses mouvements. Pour voir pourquoi il en est ainsi, considérons la position d’un point transformé p en fonction du temps ; nous désignerons cette fonction de deux matrices, d’un point et d’un temps par a(M0, M1, p, t).
Puisque dans ce cas la composante de rotation de la décomposition est l’identité, alors avec notre décomposition matricielle nous avons
où la translation et l’échelle sont toutes deux écrites comme fonctions du temps. En supposant pour simplifier que S(t) est une échelle régulière, nous pouvons trouver des expressions pour les composantes de a(M0, M1, p, t). Par exemple, pour la composante x, nous avons :
where s0,0 est l’élément correspondant de la matrice d’échelle pour M0, s0,0′ est le même élément de la matrice d’échelle pour M1, et les éléments de la matrice de translation sont désignés de façon similaire par d. (Nous avons choisi d pour « delta » ici puisque t est déjà revendiqué pour le temps.) En tant que fonction linéaire de t, les extrema de cette fonction sont aux temps de début et de fin. Les autres coordonnées et le cas d’une échelle généralisée suivent de manière similaire.
〈AnimatedTransform Method Definitions〉 + ≡
Bounds3f AnimatedTransform: :MotionBounds(const Bounds3f &b) const {
if ( !actuallyAnimated)
return (*startTransform)(b);
if (hasRotation == false)
return Union((*startTransform)(b), (*endTransform)(b)) ;
〈Return motion bounds accounting for animated rotation 108〉
}
Pour le cas général avec des rotations animées, la fonction de mouvement peut avoir des extrema à des points au milieu de la plage de temps. Nous ne connaissons pas de moyen simple pour trouver ces points. De nombreux moteurs de rendu traitent ce problème en échantillonnant un grand nombre de fois dans l’intervalle de temps, en calculant la transformation interpolée à chaque fois, et en prenant l’union de toutes les boîtes limites transformées correspondantes. Ici, nous allons développer une méthode plus fondée qui nous permet de calculer de manière robuste ces limites de mouvement.
Nous utilisons une limite conservatrice légèrement plus simple qui implique de calculer le mouvement des huit coins de la boîte englobante individuellement et de trouver l’union de ces limites.
〈Return motion bounds accounting for animated rotation〉 ≡ 108
Bounds3f bounds;
for (int corner = 0 ; corner <8 ; ++corner)
bounds = Union(bounds, BoundPointMotion(b.Corner(corner)));
return bounds;
Pour chaque coin de boîte englobante p, nous devons trouver l’extrema de a sur la plage de temps de l’animation. Rappelons que les extrema d’une fonction continue sur un certain domaine sont soit aux points limites du domaine, soit aux points où la dérivée première de la fonction est nulle. Ainsi, la borne globale est donnée par l’union des positions au début et à la fin du mouvement ainsi que la position à tout extrema.
AnimatedTransform: : actuallyAnimated 103
AnimatedTransform: : BoundPointMotion() 110
AnimatedTransform: : hasRotation 103
Bounds3::Corner() 78
Bounds3::Union() 78
Bounds3f 76
Figure 2.18 montre un tracé d’une coordonnée de la fonction de mouvement et de sa dérivée pour une trajectoire de mouvement intéressante d’un point. Notez que la valeur maximale de la fonction sur la plage de temps est atteinte à un point où la dérivée a un zéro.
Pour lier le mouvement d’un seul point, nous commençons notre dérivation en suivant l’approche utilisée pour le cas sans rotation, en développant les trois composantes T, R et S de l’équation (2.9) comme fonctions du temps et en trouvant leur produit. Nous avons :
Le résultat est assez complexe lorsqu’il est développé, principalement en raison du slerp et de la conversion du quaternion résultant en une matrice ; un système de calcul formel est nécessaire pour travailler avec cette fonction.
La dérivée ∂a(M0, M1, p, t)/∂t est également assez complexe-dans toute sa gloire algébrique, plus de 2 000 opérations sont nécessaires pour évaluer sa valeur pour une paire donnée de matrices décomposées, point et temps. Cependant, étant donné des matrices de transformation spécifiques et un point spécifique, a est considérablement simplifié ; nous désignerons la fonction spécialisée de t seul comme aM, p(t). L’évaluation de sa dérivée nécessite environ 10 opérations en virgule flottante, un sinus et un cosinus à évaluer pour chaque coordonnée :
où θ est l’arc cosinus du produit scalaire des deux quaternions et où les cinq coefficients ci sont des 3-vecteurs qui dépendent des deux matrices et de la position p. Cette spécialisation fonctionne bien, puisque nous aurons besoin d’évaluer la fonction à de nombreuses valeurs temporelles pour un point donné.
Nous avons maintenant deux tâches : d’abord, étant donné une paire de matrices d’images clés et un point p, nous devons d’abord être en mesure de calculer efficacement les valeurs des coefficients ci. Ensuite, étant donné la fonction relativement simple définie par ci et θ, nous devons trouver les zéros de l’équation (2.12), qui peuvent représenter les moments où les extrema de mouvement se produisent.
Pour la première tâche, nous allons d’abord factoriser les contributions aux coefficients qui dépendent des matrices d’images clés de celles qui dépendent du point p, en supposant que les boîtes limites pour le mouvement de plusieurs points seront calculées pour chaque paire de matrices d’images clés (comme c’est le cas ici). Le résultat est heureusement assez simple – les vecteurs ci sont sont des fonctions linéaires des composantes x, y et z du point.
Donc, étant donné les coefficients ki et un point particulier p dont nous voulons borner le mouvement, nous pouvons calculer efficacement les coefficients ci de la fonction dérivée dans l’équation (2.12). La structure DerivativeTerm encapsule ces coefficients et ce calcul.
〈AnimatedTransform Private Data〉 + ≡
struct DerivativeTerm {
DerivativeTerm(Float c, Float x, Float y, Float z)
: kc(c), kx(x), ky(y), kz(z) { }
Float kc, kx, ky, kz;
Float Eval(const Point3f &p) const {
return kc + kx * p.x + ky * p.y + kz * p.z;
}
};
Les attributs c1 – c5 stockent les informations de dérivation correspondant aux cinq termes de l’équation (2.12). Les trois éléments du tableau correspondent aux trois dimensions de l’espace.
〈AnimatedTransform Private Data〉 + ≡
Terme dérivé c1, c2, c3, c4, c5;
Le fragment 〈Compute terms of motion derivative function〉 dans le constructeur AnimatedTransform, non inclus ici, initialise ces termes, via un code généré automatiquement. Étant donné que cela nécessite quelques milliers d’opérations en virgule flottante, faire ce travail une fois et l’amortir sur les multiples coins de la boîte englobante est utile. Les coefficients ki sont plus facilement calculés si nous supposons une plage de temps canonique ; plus tard, nous devrons remapper les valeurs t des zéros de la fonction de mouvement à la plage de temps réelle de l’obturateur.
Donné les coefficients ki basés sur les matrices d’images clés, BoundPointMotion() calcule une limite robuste du mouvement de p.
〈AnimatedTransform Method Definitions〉 + ≡
Bounds3f AnimatedTransform::BoundPointMotion(const Point3f &p) const {
Bounds3f bounds((*startTransform)(p), (*endTransform)(p));
Float cosTheta = Dot(R, R);
Float theta = std: :acos(Clamp(cosTheta, -1, 1));
for (int c = 0 ; c <3 ; ++c) {
〈Trouver tous les zéros de la dérivée du mouvement pour la composante c 111〉
〈Etendre la boîte englobante pour tous les zéros de la dérivée du mouvement trouvés 111〉
}
return bounds ;
}
Bounds3f 76
DerivativeTerm 110
Float 1062
Point3f 68
La fonction IntervalFindZeros(), qui sera introduite prochainement, trouve numériquement les zéros de l’équation (2.12). Jusqu’à quatre sont possibles.
〈Trouver tous les zéros de la dérivée du mouvement pour la composante c〉 ≡ 110
Float zeros;
int nZeros = 0;
IntervalFindZeros(c1.Eval(p), c2.Eval(p), c3.Eval(p), c4.Eval(p),
c5.Eval(p), thêta,
Intervalle(0., 1.), zéros, &nZeros);
Les zéros sont trouvés sur t ∈ , nous devons donc interpoler dans l’intervalle de temps avant d’appeler la méthode pour transformer le point au temps correspondant. Notez également que l’extremum n’est qu’à l’une des dimensions x, y, et z, et donc les limites ne doivent être mises à jour que dans cette seule dimension. Par commodité, nous utilisons ici simplement la fonction Union(), qui prend en compte toutes les dimensions, même si deux pourraient être ignorées.
〈Expansion de la boîte englobante pour tout mouvement dérivé des zéros trouvés〉 ≡ 110
for (int i = 0 ; i <nZeros ; ++i) {
Point3f pz = (*this)(Lerp(zeros, startTime, endTime), p) ;
bounds = Union(bounds, pz);
}
La recherche des zéros de la fonction dérivée du mouvement, équation (2.12), ne peut pas être fait algébriquement ; des méthodes numériques sont nécessaires. Heureusement, la fonction se comporte bien : elle est assez lisse et possède un nombre limité de zéros. (Rappelez-vous le tracé de la figure 2.18, qui était un représentant inhabituellement complexe.)
Bien que nous puissions utiliser une recherche basée sur la bissection ou la méthode de Newton, nous risquerions de manquer des zéros lorsque la fonction ne traverse que brièvement l’axe. Par conséquent, nous utiliserons l’arithmétique d’intervalle, une extension de l’arithmétique qui donne un aperçu du comportement des fonctions sur des plages de valeurs, ce qui permet de trouver de manière robuste les zéros des fonctions.
Pour comprendre l’idée de base de l’arithmétique d’intervalle, considérons, par exemple, la fonction f (x) = 2x. Si nous avons un intervalle de valeurs ∈ ℝ, alors nous pouvons voir que sur l’intervalle, l’étendue de f est l’intervalle . En d’autres termes, f () ⊂ .
Plus généralement, toutes les opérations de base de l’arithmétique ont des extensions d’intervalle qui décrivent comment elles opèrent sur les intervalles. Par exemple, étant donné deux intervalles et ,
En d’autres termes, si nous additionnons deux valeurs dont l’une est dans l’intervalle et la seconde dans , alors le résultat doit être dans l’intervalle ,
L’arithmétique d’intervalle a la propriété importante que les intervalles qu’elle donne sont conservatifs. En particulier, si f () ⊂ et si c > 0, alors on sait avec certitude qu’aucune valeur de dans ne fait que f soit négative. Dans ce qui suit, nous montrerons comment calculer l’équation (2.12) sur des intervalles et nous profiterons des limites conservatrices des intervalles calculés pour trouver efficacement de petits intervalles avec des passages à zéro où les méthodes régulières de recherche de racines peuvent être utilisées de manière fiable.
D’abord, nous définirons une classe Intervalle qui représente les intervalles de nombres réels.
Bounds3: :Union() 78
Float 1062
Intervalle 112
IntervalleFindZeros() 113
Lerp() 1079
.
Point3f 68
〈Intervalle Définitions〉 ≡
classe Intervalle {
public :
〈Interval Public Methods 112〉
Float low, high;
};
Un intervalle peut être initialisé avec une seule valeur, représentant un seul point sur la ligne des nombres réels, ou avec deux valeurs qui spécifient un intervalle avec une largeur non nulle.
〈Interval Public Methods〉 ≡ 112
Interval(Float v) : low(v), high(v) { }
Interval(Float v0, Float v1)
: low(std::min(v0, v1)), high(std::max(v0, v1)) { }
La classe fournit également des surcharges pour les opérations arithmétiques de base. Notez que pour la soustraction, la valeur haute du deuxième intervalle est soustraite de la valeur basse du premier3.
〈Interval Public Methods〉 + ≡ 112
Interval operator + (const Interval &i) const {
return Interval(low + i.low, high + i.high);
}
Intervalle operator-(const Interval &i) const {
return Interval(low – i.high, high – i.low);
}
Pour la multiplication, les côtés de chaque intervalle qui déterminent les valeurs minimale et maximale de l’intervalle résultat dépendent des signes des valeurs respectives. Multiplier les différentes possibilités et prendre le minimum et le maximum global est plus facile que de travailler sur celles à utiliser et de les multiplier.
〈Interval Public Methods〉 + ≡ 112
Interval operator*(const Interval &i) const {
return Interval(std::min(std::min(low * i.low, high * i.low),
std::min(low * i.high, high * i.high)),
std::max(std::max(low * i.low, high * i.low),
std::max(low * i.high, high * i.high))) ;
}
Nous avons également implémenté les fonctions Sin() et Cos() pour les intervalles. Les implémentations supposent que l’intervalle donné est dans , ce qui est le cas pour notre utilisation de ces fonctions. Nous n’incluons ici que l’implémentation de Sin() ; Cos() est assez similaire dans sa structure de base.
Float 1062
Intervalle 112
Intervalle::high 112
Intervalle: :low 112
〈Intervalle Définitions〉 + ≡
inline Intervalle Sin(const Intervalle &i) {
Float sinLow = std::sin(i.low), sinHigh = std::sin(i.high);
if (sinLow> sinHigh)
std::swap(sinLow, sinHigh);
if (i.low <Pi / 2 && i.high> Pi / 2)
sinHigh = 1.;
if (i.low <(3.f / 2.f) * Pi && i.high> (3.f / 2.f) * Pi)
sinLow = -1.;
return Interval(sinLow, sinHigh);
}
Donné la machinerie d’intervalle, nous pouvons maintenant implémenter la fonction IntervalFindZeros(), qui trouve les valeurs t de tout passage à zéro de l’équation (2.12) sur l’intervalle tInterval donné.
〈Intervalle Définitions〉 + ≡
void IntervalFindZeros(Float c1, Float c2, Float c3, Float c4,
Float c5, Float theta, Intervalle tInterval, Float *zeros,
int *zeroCount, int depth = 8) {
〈Evaluer la dérivée du mouvement sous forme d’intervalle, retour si pas de zéros 113〉
if (depth> 0) {
〈Split tInterval et vérifie les deux intervalles résultants 114〉
} else {
〈Utiliser la méthode de Newton pour affiner le zéro 114〉
}
}
La fonction commence par calculer la plage d’intervalles sur tInterval. Si l’étendue ne couvre pas zéro, alors il n’y a pas de zéros de la fonction sur tInterval et la fonction peut retourner.
〈Evaluer la dérivée du mouvement sous forme d’intervalle, retour si pas de zéros〉 ≡ 113
Etendue de l’intervalle = Intervalle(c1) +
(Intervalle(c2) + Intervalle(c3) * tInterval) *
Cos(Intervalle(2 * thêta) * tInterval) +
(Intervalle(c4) + Intervalle(c5) * tInterval) *
Sin(Intervalle(2 * thêta) * tInterval) ;
if (range.low> 0. ∥ range.high <0. ∥ range.low == range.high)
return;
Si la plage d’intervalles s’étend bien sur zéro, alors il peut y avoir un ou plusieurs zéros dans l’intervalle tInterval, mais il est aussi possible qu’il n’y en ait en fait aucun, puisque les limites de l’intervalle sont conservatrices mais pas aussi serrées que possible. La fonction divise tInterval en deux parties et vérifie récursivement les deux sous-intervalles. La réduction de la taille du domaine de l’intervalle réduit généralement l’étendue de l’intervalle, ce qui peut nous permettre de déterminer qu’il n’y a pas de zéros dans l’un ou les deux nouveaux intervalles.
Float 1062
Intervalle 112
Intervalle::haut 112
Intervalle: :low 112
Pi 1063
〈Split tInterval et vérifie les deux intervalles résultants〉 ≡ 113
Float mid = (tInterval.low + tInterval.high) * 0.5f;
IntervalFindZeros(c1, c2, c3, c4, c5, theta,
Intervalle(tInterval.low, mid), zeros, zeroCount, depth – 1);
IntervalFindZeros(c1, c2, c3, c4, c5, theta,
Intervalle(mid, tInterval.high), zeros, zeroCount, depth – 1);
Une fois que nous avons un intervalle étroit où la valeur d’intervalle de la fonction dérivée du mouvement s’étend sur zéro, l’implémentation passe à quelques itérations de la méthode de Newton pour trouver le zéro, en commençant au milieu de l’intervalle. La méthode de Newton nécessite la dérivée de la fonction ; puisque nous trouvons les zéros de la fonction dérivée du mouvement, c’est la dérivée seconde de l’équation (2.11) :
〈Utiliser la méthode de Newton pour raffiner le zéro〉 ≡ 113
Float tNewton = (tInterval.low + tInterval.high) * 0.5f;
for (int i = 0 ; i <4 ; ++i) {
Float fNewton = c1 +
(c2 + c3 * tNewton) * std::cos(2.f * thêta * tNewton) +
(c4 + c5 * tNewton) * std::sin(2.f * thêta * tNewton);
Float fPrimeNewton =
(c3 + 2 * (c4 + c5 * tNewton) * thêta) *
std::cos(2.f * tNewton * thêta) +
(c5 – 2 * (c2 + c3 * tNewton) * thêta) *
std::sin(2.f * tNewton * thêta);
si (fNewton == 0 ∥ fPrimeNewton == 0)
break;
tNewton = tNewton – fNewton / fPrimeNewton;