2.9.4 Bounding bewegte Bounding Boxen
Bei einer Bounds3f, die durch eine animierte Transformation transformiert wird, ist es nützlich, eine Bounding Box berechnen zu können, die die gesamte Bewegung über die Animationszeitspanne einschließt. Wenn wir beispielsweise die Bewegung eines animierten geometrischen Primitivs begrenzen können, dann können wir Strahlen mit dieser Begrenzung schneiden, um festzustellen, ob der Strahl das Objekt schneiden könnte, bevor die Kosten für die Interpolation der Begrenzung des Primitivs auf die Zeit des Strahls anfallen, um diesen Schnitt zu überprüfen. Die Methode AnimatedTransform::MotionBounds() führt diese Berechnung durch, nimmt eine Bounding Box und gibt die Bounding Box ihrer Bewegung über den Zeitbereich der AnimatedTransform zurück.
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
RayDifferential 75
Slerp() 103
Transform 83
Translate() 87
Vector3f 60
Es gibt zwei einfache Fälle: Erstens, wenn die Keyframe-Matrizen gleich sind, können wir willkürlich nur die Starttransformation anwenden, um die vollständigen Grenzen zu berechnen. Zweitens, wenn die Transformation nur Skalierung und/oder Translation umfasst, dann begrenzt der Begrenzungsrahmen, der die transformierten Positionen des Begrenzungsrahmens sowohl zum Start- als auch zum Endzeitpunkt einschließt, die gesamte Bewegung. Um zu sehen, warum dies so ist, betrachten wir die Position eines transformierten Punktes p als eine Funktion der Zeit; wir bezeichnen diese Funktion von zwei Matrizen, einem Punkt und einer Zeit mit a(M0, M1, p, t).
Da in diesem Fall die Rotationskomponente der Zerlegung die Identität ist, haben wir mit unserer Matrixzerlegung
wobei die Translation und die Skalierung beide als Funktionen der Zeit geschrieben werden. Wenn wir der Einfachheit halber annehmen, dass S(t) eine reguläre Skala ist, können wir Ausdrücke für die Komponenten von a(M0, M1, p, t) finden. Für die x-Komponente ergibt sich zum Beispiel:
where s0,0 das entsprechende Element der Skalenmatrix für M0 ist, s0,0′ das gleiche Skalenmatrixelement für M1 ist und die Elemente der Translationsmatrix ebenfalls mit d bezeichnet werden. (Wir haben hier d für „delta“ gewählt, da t bereits für die Zeit steht.) Als lineare Funktion von t liegen die Extrema dieser Funktion bei den Start- und Endzeiten. Die anderen Koordinaten und der Fall für eine verallgemeinerte Skala folgen auf ähnliche Weise.
〈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〉
}
Für den allgemeinen Fall mit animierten Rotationen kann die Bewegungsfunktion Extrema an Punkten in der Mitte des Zeitbereichs haben. Wir kennen keine einfache Methode, um diese Punkte zu finden. Viele Renderer lösen dieses Problem, indem sie eine große Anzahl von Zeitpunkten im Zeitbereich abtasten, die interpolierte Transformation an jedem dieser Punkte berechnen und die Vereinigung aller entsprechenden transformierten Bounding Boxes vornehmen. Hier werden wir eine fundiertere Methode entwickeln, mit der wir diese Bewegungsgrenzen robust berechnen können.
Wir verwenden eine etwas einfachere konservative Grenze, bei der die Bewegung der acht Ecken des Begrenzungsrahmens einzeln berechnet wird und die Vereinigung dieser Grenzen gefunden wird.
〈Rückgabe der Bewegungsgrenzen unter Berücksichtigung der animierten Rotation〉 ≡ 108
Bounds3f bounds;
for (int corner = 0; corner <8; ++corner)
bounds = Union(bounds, BoundPointMotion(b.Corner(corner)));
return bounds;
Für jede Bounding-Box-Ecke p müssen wir die Extrema von a über den Animationszeitbereich finden. Aus der Infinitesimalrechnung wissen wir, dass die Extrema einer stetigen Funktion über einem Bereich entweder an den Randpunkten des Bereichs oder an den Punkten liegen, an denen die erste Ableitung der Funktion Null ist. Somit ist die Gesamtbegrenzung durch die Vereinigung der Positionen am Anfang und am Ende der Bewegung sowie der Position an jedem Extremum gegeben.
AnimatedTransform:: actualAnimated 103
AnimatedTransform:: BoundPointMotion() 110
AnimatedTransform:: hasRotation 103
Bounds3::Corner() 78
Bounds3::Union() 78
Bounds3f 76
Abbildung 2.18 zeigt eine Darstellung einer Koordinate der Bewegungsfunktion und ihrer Ableitung für einen interessanten Bewegungspfad eines Punktes. Man beachte, dass der Maximalwert der Funktion über den Zeitbereich an einem Punkt erreicht wird, an dem die Ableitung eine Null hat.
Um die Bewegung eines einzelnen Punktes zu bestimmen, beginnen wir unsere Ableitung, indem wir dem Ansatz folgen, der für den Fall ohne Rotation verwendet wurde, indem wir die drei T-, R- und S-Komponenten von Gleichung (2.9) als Funktionen der Zeit erweitern und ihr Produkt finden. Wir haben:
Das Ergebnis ist ziemlich komplex, wenn man es ausdehnt, vor allem wegen des Slerp und der Umwandlung des resultierenden Quaternions in eine Matrix; ein Computeralgebrasystem ist eine Voraussetzung für die Arbeit mit dieser Funktion.
Die Ableitung ∂a(M0, M1, p, t)/∂t ist ebenfalls recht komplex – in ihrer vollen algebraischen Pracht sind über 2.000 Operationen erforderlich, um ihren Wert für ein gegebenes Paar zerlegter Matrizen, Punkt und Zeit, zu ermitteln. Bei bestimmten Transformationsmatrizen und einem bestimmten Punkt wird a jedoch erheblich vereinfacht; wir bezeichnen die spezielle Funktion von t allein als aM, p(t). Die Auswertung ihrer Ableitung erfordert etwa 10 Gleitkommaoperationen, einen Sinus und einen Kosinus für jede Koordinate:
wobei θ der Arcuskosinus des Punktprodukts der beiden Quaternionen ist und die fünf Koeffizienten ci 3-Vektoren sind, die von den beiden Matrizen und der Position p abhängen. Diese Spezialisierung hat sich bewährt, da wir die Funktion für einen gegebenen Punkt bei vielen Zeitwerten auswerten müssen.
Wir haben nun zwei Aufgaben: Zunächst müssen wir bei einem Paar von Keyframe-Matrizen und einem Punkt p in der Lage sein, die Werte der Koeffizienten ci effizient zu berechnen. Dann müssen wir angesichts der relativ einfachen Funktion, die durch ci und θ definiert ist, die Nullstellen von Gleichung (2.12) finden, die die Zeitpunkte darstellen können, zu denen Bewegungsextrema auftreten.
Für die erste Aufgabe werden wir zunächst die Beiträge zu den Koeffizienten, die von den Keyframe-Matrizen abhängen, aus denen herausrechnen, die von dem Punkt p abhängen, unter der Annahme, dass Bounding Boxes für die Bewegung mehrerer Punkte für jedes Paar von Keyframe-Matrizen berechnet werden (wie es hier der Fall ist). Das Ergebnis ist glücklicherweise recht einfach – die ci-Vektoren sind lineare Funktionen der x-, y- und z-Komponenten des Punktes.
Angesichts der ki-Koeffizienten und eines bestimmten Punktes p, dessen Bewegung wir begrenzen wollen, können wir also die Koeffizienten ci der Ableitungsfunktion in Gleichung (2.12) effizient berechnen. Die Struktur DerivativeTerm kapselt diese Koeffizienten und diese Berechnung.
〈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;
}
};
Die Attribute c1 – c5 speichern Ableitungsinformationen entsprechend den fünf Termen in Gleichung (2.12). Die drei Array-Elemente entsprechen den drei Dimensionen des Raums.
〈AnimatedTransform Private Data〉 + ≡
DerivativeTerm c1, c2, c3, c4, c5;
Das Fragment 〈Compute terms of motion derivative function〉 im AnimatedTransform-Konstruktor, das hier nicht enthalten ist, initialisiert diese Terme über automatisch generierten Code. Da dafür einige tausend Fließkommaoperationen erforderlich sind, ist es hilfreich, diese Arbeit einmal zu erledigen und über mehrere Bounding-Box-Ecken zu amortisieren. Die Koeffizienten ki lassen sich leichter berechnen, wenn wir von einem kanonischen Zeitbereich ausgehen; später müssen wir die t-Werte der Nullen der Bewegungsfunktion auf den tatsächlichen Verschlusszeitbereich umrechnen.
Gegeben die Koeffizienten ki auf der Grundlage der Keyframe-Matrizen, berechnet BoundPointMotion() eine robuste Begrenzung der Bewegung von 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(Klammer(cosTheta, -1, 1));
for (int c = 0; c <3; ++c) {
〈Finde alle Bewegungsableitungs-Nullstellen für die Komponente c 111〉
〈Erweiterung des Begrenzungsrahmens für alle gefundenen Bewegungsableitungs-Nullstellen 111〉
}
return bounds;
}
Bounds3f 76
DerivativeTerm 110
Float 1062
Point3f 68
Die Funktion IntervalFindZeros(), die in Kürze vorgestellt wird, findet numerisch Nullstellen von Gleichung (2.12). Bis zu vier sind möglich.
〈Finde beliebige Bewegungsableitungs-Nullstellen für die Komponente c〉 ≡ 110
Float-Nullstellen;
int nZeros = 0;
IntervalFindZeros(c1.Eval(p), c2.Eval(p), c3.Eval(p), c4.Eval(p),
c5.Eval(p), theta,
Intervall(0., 1.), Nullen, &nZeros);
Die Nullen werden über t ∈ gefunden, so dass wir innerhalb des Zeitbereichs interpolieren müssen, bevor wir die Methode zur Transformation des Punktes zum entsprechenden Zeitpunkt aufrufen. Beachten Sie auch, dass das Extremum nur in einer der Dimensionen x, y und z liegt, so dass die Schranken nur in dieser einen Dimension aktualisiert werden müssen. Der Einfachheit halber verwenden wir hier nur die Funktion Union(), die alle Dimensionen berücksichtigt, auch wenn zwei ignoriert werden könnten.
〈Erweiterung des Begrenzungsrahmens für jede gefundene Bewegungsableitung Nullen〉 ≡ 110
for (int i = 0; i <nZeros; ++i) {
Point3f pz = (*this)(Lerp(zeros, startTime, endTime), p);
bounds = Union(bounds, pz);
}
Das Finden von Nullstellen der Bewegungsableitungsfunktion, Gleichung (2.12), kann nicht algebraisch erfolgen; es sind numerische Methoden erforderlich. Glücklicherweise verhält sich die Funktion gut – sie ist ziemlich glatt und hat eine begrenzte Anzahl von Nullstellen. (Erinnern Sie sich an die Darstellung in Abbildung 2.18, die ein ungewöhnlich komplexer Vertreter war.)
Wir könnten zwar eine auf der Bisektion basierende Suche oder die Newton-Methode verwenden, aber wir würden riskieren, Nullen zu übersehen, wenn die Funktion die Achse nur kurz kreuzt. Daher werden wir die Intervallarithmetik verwenden, eine Erweiterung der Arithmetik, die Einblicke in das Verhalten von Funktionen über Wertebereiche gibt, was es ermöglicht, Nullstellen von Funktionen robust zu finden.
Um die Grundidee der Intervallarithmetik zu verstehen, betrachten wir zum Beispiel die Funktion f (x) = 2x. Wenn wir ein Intervall von Werten ∈ ℝ haben, dann können wir sehen, dass über dem Intervall, der Bereich von f das Intervall ist. Mit anderen Worten: f () ⊂ .
Allerdings haben alle Grundrechenarten Intervallerweiterungen, die beschreiben, wie sie auf Intervallen arbeiten. Bei zwei Intervallen und ,
Mit anderen Worten: Wenn wir zwei Werte addieren, von denen einer im Bereich und der zweite im Bereich liegt, dann muss das Ergebnis im Bereich liegen.
Die Intervallarithmetik hat die wichtige Eigenschaft, dass die Intervalle, die sie liefert, konservativ sind. Insbesondere, wenn f () ⊂ und c > 0 ist, dann wissen wir sicher, dass kein Wert in f negativ ist. Im Folgenden wird gezeigt, wie man Gleichung (2.12) über Intervalle berechnet und die konservativen Schranken der berechneten Intervalle nutzt, um effizient kleine Intervalle mit Nulldurchgängen zu finden, in denen reguläre Wurzelfindungsmethoden zuverlässig eingesetzt werden können.
Zunächst wird eine Intervallklasse definiert, die Intervalle aus reellen Zahlen darstellt.
Bounds3::Union() 78
Float 1062
Interval 112
IntervalFindZeros() 113
Lerp() 1079
Point3f 68
〈Interval Definitions〉 ≡
class Interval {
public:
〈Interval Public Methods 112〉
Float low, high;
};
Ein Intervall kann mit einem einzelnen Wert initialisiert werden, der einen einzelnen Punkt auf der reellen Zahlengeraden repräsentiert, oder mit zwei Werten, die ein Intervall mit einer Breite ungleich Null angeben.
〈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)) { }
Die Klasse bietet auch Überladungen für die grundlegenden arithmetischen Operationen. Beachten Sie, dass bei der Subtraktion der hohe Wert des zweiten Intervalls vom niedrigen Wert des ersten subtrahiert wird.3
〈Interval Public Methods〉 + ≡ 112
Interval operator + (const Interval &i) const {
return Interval(low + i.low, high + i.high);
}
Interval operator-(const Interval &i) const {
return Interval(low – i.high, high – i.low);
}
Für die Multiplikation hängt es von den Vorzeichen der jeweiligen Werte ab, welche Seiten eines Intervalls den Minimal- und Maximalwert des Ergebnisintervalls bestimmen. Es ist einfacher, die verschiedenen Möglichkeiten zu multiplizieren und das Gesamtminimum und -maximum zu nehmen, als zu überlegen, welche zu verwenden sind und diese zu multiplizieren.
〈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)));
}
Wir haben auch Sin() und Cos() Funktionen für Intervalle implementiert. Die Implementierungen gehen davon aus, dass das angegebene Intervall in ist, was für unsere Verwendung dieser Funktionen der Fall ist. Wir geben hier nur die Implementierung von Sin() wieder; Cos() ist von der Grundstruktur her recht ähnlich.
Float 1062
Intervall 112
Intervall::high 112
Intervall::low 112
〈Interval Definitions〉 + ≡
inline Interval Sin(const Interval &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);
}
Angesichts der Intervallmaschinerie können wir nun die Funktion IntervalFindZeros() implementieren, die die t-Werte aller Nulldurchgänge von Gleichung (2.12) über das gegebene Intervall tInterval findet.
〈Intervall Definitionen〉 + ≡
void IntervalFindZeros(Float c1, Float c2, Float c3, Float c4,
Float c5, Float theta, Intervall tInterval, Float *zeros,
int *zeroCount, int depth = 8) {
〈Auswerten der Bewegungsableitung in Intervallform, return if no zeros 113〉
if (depth> 0) {
〈Split tInterval and check both resulting intervals 114〉
} else {
〈Verwendung der Newton-Methode zur Verfeinerung der Null 114〉
}
}
Die Funktion beginnt mit der Berechnung des Intervallbereichs über tInterval. Wenn der Bereich nicht die Null umfasst, dann gibt es keine Nullen der Funktion über tInterval und die Funktion kann zurückkehren.
〈Auswerten der Bewegungsableitung in Intervallform, return if no zeros〉 ≡ 113
Intervallbereich = Interval(c1) +
(Interval(c2) + Interval(c3) * tInterval) *
Cos(Interval(2 * theta) * tInterval) +
(Interval(c4) + Interval(c5) * tInterval) *
Sin(Interval(2 * theta) * tInterval);
if (range.low> 0. ∥ range.high <0. ∥ range.low == range.high)
return;
Wenn der Intervallbereich die Null überspannt, dann kann es eine oder mehrere Nullen im Intervall tInterval geben, aber es ist auch möglich, dass es tatsächlich keine gibt, da die Intervallgrenzen konservativ aber nicht so eng wie möglich sind. Die Funktion teilt tInterval in zwei Teile auf und prüft rekursiv die beiden Teilintervalle. Die Verkleinerung des Intervallbereichs verringert im Allgemeinen die Ausdehnung des Intervallbereichs, was es uns ermöglichen kann, festzustellen, dass es in einem oder beiden der neuen Intervalle keine Nullen gibt.
Float 1062
Intervall 112
Intervall::hoch 112
Intervall::low 112
Pi 1063
〈Split tInterval und Überprüfung der beiden resultierenden Intervalle〉 ≡ 113
Float mid = (tInterval.low + tInterval.high) * 0.5f;
IntervalFindZeros(c1, c2, c3, c4, c5, theta,
Interval(tInterval.low, mid), zeros, zeroCount, depth – 1);
IntervalFindZeros(c1, c2, c3, c4, c5, theta,
Interval(mid, tInterval.high), zeros, zeroCount, depth – 1);
Sobald wir ein enges Intervall haben, in dem der Intervallwert der Bewegungsableitungsfunktion den Nullpunkt überspannt, schaltet die Implementierung auf einige Iterationen der Newton-Methode um, um den Nullpunkt zu finden, beginnend in der Mitte des Intervalls. Die Newton-Methode erfordert die Ableitung der Funktion; da wir Nullstellen der Bewegungsableitungsfunktion finden, ist dies die zweite Ableitung von Gleichung (2.11):
〈Verwendung der Newton-Methode zur Verfeinerung von zero〉 ≡ 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 * theta * tNewton) +
(c4 + c5 * tNewton) * std::sin(2.f * theta * tNewton);
Float fPrimeNewton =
(c3 + 2 * (c4 + c5 * tNewton) * theta) *
std::cos(2.f * tNewton * theta) +
(c5 – 2 * (c2 + c3 * tNewton) * theta) *
std::sin(2.f * tNewton * theta);
if (fNewton == 0 ∥ fPrimeNewton == 0)
break;
tNewton = tNewton – fNewton / fPrimeNewton;