Bounding Box

2.9.4 Bounding moving bounding boxes

Dado un Bounds3f que es transformado por una transformación animada, es útil ser capaz de calcular una caja delimitadora que abarque todo su movimiento durante el período de tiempo de la animación. Por ejemplo, si podemos delimitar el movimiento de una primitiva geométrica animada, entonces podemos intersecar rayos con este límite para determinar si el rayo podría intersecar el objeto antes de incurrir en el coste de interpolar el límite de la primitiva al tiempo del rayo para comprobar esa intersección. El método AnimatedTransform::MotionBounds() realiza este cálculo, tomando un cuadro delimitador y devolviendo el cuadro delimitador de su movimiento sobre el rango de tiempo de 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

RayDifferential 75

Slerp() 103

Transform 83

Translate() 87

Vector3f 60

Hay dos casos fáciles: primero, si las matrices de fotogramas clave son iguales, entonces podemos aplicar arbitrariamente sólo la transformación inicial para calcular los límites completos. En segundo lugar, si la transformación sólo incluye la escala y/o la traslación, entonces el cuadro delimitador que abarca las posiciones transformadas del cuadro delimitador tanto en el momento inicial como en el final delimita todo su movimiento. Para ver por qué esto es así, considera la posición de un punto transformado p como una función del tiempo; denotaremos esta función de dos matrices, un punto y un tiempo por a(M0, M1, p, t).

Como en este caso la componente de rotación de la descomposición es la identidad, entonces con nuestra descomposición matricial tenemos

aM0M1pt=TtStp,

donde la traslación y la escala se escriben ambas como funciones del tiempo. Suponiendo para simplificar que S(t) es una escala regular, podemos encontrar expresiones para las componentes de a(M0, M1, p, t). Por ejemplo, para la componente x, tenemos:

aM0M1ptx=1−ts0,0+ts0,0′px+1−td0,3+td0,3′=s0,0px+d0,3+−s0,0px+s0,0′px−d0,3+d0,3′t,

where s0,0 es el elemento correspondiente de la matriz de escala para M0, s0,0′ es el mismo elemento de la matriz de escala para M1, y los elementos de la matriz de traslación se denotan igualmente por d. (Aquí elegimos d por «delta», ya que t ya se reivindica como tiempo.) Como función lineal de t, los extremos de esta función se encuentran en los tiempos de inicio y final. ¡Las otras coordenadas y el caso de una escala generalizada siguen de manera similar.

〈Definiciones del método AnimatedTransform〉 + ≡

Bounds3f AnimatedTransform::MotionBounds(const Bounds3f &b) const {

if (!actuallyAnimated)

return (*startTransform)(b);

if (hasRotation == false)

return Union((*startTransform)(b), (*endTransform)(b));

〈Retorno de los límites de movimiento teniendo en cuenta la rotación animada 108〉

}

Para el caso general con rotaciones animadas, la función de movimiento puede tener extremos en puntos en el medio del rango de tiempo. No conocemos ninguna forma sencilla de encontrar estos puntos. Muchos renderizadores abordan este problema muestreando un gran número de veces en el rango de tiempo, calculando la transformación interpolada en cada uno de ellos y tomando la unión de todos los cuadros delimitadores transformados correspondientes. Aquí desarrollaremos un método más fundamentado que nos permite calcular de forma robusta estos límites de movimiento.

Utilizamos un límite conservador ligeramente más sencillo que implica calcular el movimiento de las ocho esquinas del cuadro delimitador individualmente y encontrar la unión de esos límites.

〈Devuelve los límites de movimiento teniendo en cuenta la rotación animada〉 ≡ 108

Bounds3f bounds;

for (int corner = 0; corner <8; ++corner)

bounds = Union(bounds, BoundPointMotion(b.Corner(corner));

return bounds;

Para cada esquina de la caja delimitadora p, necesitamos encontrar el extremo de a sobre el rango de tiempo de la animación. Recordemos que los extremos de una función continua sobre un dominio se encuentran en los puntos límite del dominio o en los puntos donde la primera derivada de la función es cero. Por lo tanto, el límite global está dado por la unión de las posiciones al inicio y al final del movimiento, así como la posición en cualquier extremo.

AnimatedTransform:: actuallyAnimated 103

AnimatedTransform:: BoundPointMotion() 110

AnimatedTransform:: hasRotation 103

Bounds3::Corner() 78

Bounds3::Union() 78

Bounds3f 76

La figura 2.18 muestra un gráfico de una coordenada de la función de movimiento y su derivada para una trayectoria de movimiento interesante de un punto. Obsérvese que el valor máximo de la función en el intervalo de tiempo se alcanza en un punto donde la derivada tiene un cero.

Figura 2.18. (a) Movimiento de la coordenada x de un punto p en función del tiempo, determinado por dos matrices de fotogramas clave. (b) La derivada de la función de movimiento, Ecuación (2.12). Obsérvese que los extremos de la función de movimiento en el rango de tiempo dado corresponden a ceros de la derivada.

Para acotar el movimiento de un solo punto, comenzamos nuestra derivación siguiendo el enfoque utilizado para el caso sin rotación, expandiendo los tres componentes T, R y S de la Ecuación (2.9) como funciones del tiempo y encontrando su producto. Tenemos:

aM0M1pt=TtRtStp.

El resultado es bastante complejo cuando se expande, sobre todo debido al slerp y la conversión del cuaternión resultante a una matriz; un sistema de álgebra computacional es un requisito para trabajar con esta función.

La derivada ∂a(M0, M1, p, t)/∂t también es bastante compleja-en toda su gloria algebraica, se requieren más de 2.000 operaciones para evaluar su valor para un par dado de matrices descompuestas, punto y tiempo. Sin embargo, dadas las matrices de transformación específicas y un punto específico, a se simplifica sustancialmente; denotaremos la función especializada de t solo como aM, p(t). La evaluación de su derivada requiere aproximadamente 10 operaciones de punto flotante, un seno y un coseno para evaluar cada coordenada:

daM,ptdt=c1+c2+c3tcos2θt+c4+c5tsin2θt,

donde θ es el arco coseno del producto punto de los dos cuaterniones y donde los cinco coeficientes ci son 3-vectores que dependen de las dos matrices y la posición p. Esta especialización funciona bien, ya que necesitaremos evaluar la función en muchos valores de tiempo para un punto dado.

Ahora tenemos dos tareas: primero, dado un par de matrices de fotogramas clave y un punto p, primero necesitamos ser capaces de calcular eficientemente los valores de los coeficientes ci. Luego, dada la función relativamente sencilla definida por ci y θ, necesitamos encontrar los ceros de la ecuación (2.12), que pueden representar los momentos en los que se producen los extremos del movimiento.

Para la primera tarea, primero factorizaremos las contribuciones a los coeficientes que dependen de las matrices de fotogramas clave de las que dependen del punto p, bajo el supuesto de que las cajas de delimitación para el movimiento de múltiples puntos se calcularán para cada par de matrices de fotogramas clave (como es el caso aquí). El resultado es, afortunadamente, bastante sencillo: los vectores ci son funciones lineales de los componentes x, y y z del punto.

cip=ki,c+ki,xpx+ki,ypy+ki,zpz.

Así, dados los coeficientes ki y un punto p concreto del que queremos acotar el movimiento, podemos calcular de forma eficiente los coeficientes ci de la función derivativa de la ecuación (2.12). La estructura DerivativeTerm encapsula estos coeficientes y este cálculo.

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

}

};

Los atributos c1 – c5 almacenan la información de las derivadas correspondientes a los cinco términos de la ecuación (2.12). Los tres elementos del array corresponden a las tres dimensiones del espacio.

〈AnimatedTransform Private Data〉 + ≡

DerivativeTerm c1, c2, c3, c4, c5;

El fragmento 〈Compute terms of motion derivative function〉 en el constructor de AnimatedTransform, no incluido aquí, inicializa estos términos, mediante código generado automáticamente. Dado que requiere unos cuantos miles de operaciones de punto flotante, hacer este trabajo una vez y amortizarlo sobre las múltiples esquinas de la caja delimitadora es útil. Los coeficientes ki se calculan más fácilmente si asumimos un rango de tiempo canónico ; más tarde, tendremos que reasignar los valores t de los ceros de la función de movimiento al rango de tiempo real del obturador.

Dados los coeficientes ki basados en las matrices de fotogramas clave, BoundPointMotion() calcula un límite robusto del movimiento 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) {

〈Encuentra cualquier cero de la derivada de movimiento para el componente c 111〉

〈Expande la caja delimitadora para cualquier cero de la derivada de movimiento encontrado 111〉

}

return bounds;

}

Bounds3f 76

DerivativeTerm 110

Float 1062

Point3f 68

La función IntervalFindZeros(), que se presentará en breve, encuentra numéricamente los ceros de la ecuación (2.12). Hasta cuatro son posibles.

〈Encuentra los ceros de cualquier derivada del movimiento para el componente c〉 ≡ 110

Float zeros;

int nZeros = 0;

IntervalFindZeros(c1.Eval(p), c2.Eval(p), c3.Eval(p), c4.Eval(p),

c5.Eval(p), theta,

Intervalo(0., 1.), ceros, &nCeros);

Los ceros se encuentran sobre t ∈ , por lo que necesitamos interpolar dentro del rango de tiempo antes de llamar al método para transformar el punto en el tiempo correspondiente. Obsérvese también que el extremo está sólo en una de las dimensiones x, y y z, por lo que los límites sólo necesitan ser actualizados en esa dimensión. Por conveniencia, aquí sólo usamos la función Union(), que considera todas las dimensiones, aunque dos podrían ser ignoradas.

〈Expandir la caja delimitadora para cualquier derivada de movimiento de los ceros encontrada〉 ≡ 110

for (int i = 0; i <nCeros; ++i) {

Point3f pz = (*this)(Lerp(zeros, startTime, endTime), p);

Límites = Unión(límites, pz);

}

Encontrar los ceros de la función derivada del movimiento, Ecuación (2.12), no puede hacerse algebraicamente; son necesarios métodos numéricos. Afortunadamente, la función se comporta bien – es bastante suave y tiene un número limitado de ceros. (Recuerde el gráfico de la figura 2.18, que era un representante inusualmente complejo.)

Aunque podríamos utilizar una búsqueda basada en la bisección o el método de Newton, nos arriesgaríamos a perder ceros cuando la función sólo cruza brevemente el eje. Por lo tanto, utilizaremos la aritmética de intervalos, una extensión de la aritmética que permite conocer el comportamiento de las funciones sobre rangos de valores, lo que hace posible encontrar de forma robusta los ceros de las funciones.

Para entender la idea básica de la aritmética de intervalos, consideremos, por ejemplo, la función f (x) = 2x. Si tenemos un intervalo de valores ∈ ℝ, entonces podemos ver que sobre el intervalo, el rango de f es el intervalo . En otras palabras f () ⊂ .

De forma más general, todas las operaciones básicas de la aritmética tienen extensiones de intervalo que describen cómo operan sobre intervalos. Por ejemplo, dados dos intervalos y ,

ab+cd⊂a+c,b+d.

En otras palabras, si sumamos dos valores donde uno está en el intervalo y el segundo está en , entonces el resultado debe estar en el intervalo.

La aritmética de intervalos tiene la importante propiedad de que los intervalos que da son conservativos. En particular, si f () ⊂ y si c > 0, entonces sabemos con seguridad que ningún valor en hace que f sea negativo. A continuación, mostraremos cómo calcular la ecuación (2.12) sobre intervalos y aprovecharemos los límites conservativos de los intervalos calculados para encontrar eficientemente intervalos pequeños con cruces de cero en los que los métodos regulares de búsqueda de raíces pueden utilizarse de forma fiable.

Primero definiremos una clase Intervalos que representa intervalos de números reales.

Bounds3::Union() 78

Float 1062

Interval 112

IntervalFindZeros() 113

Lerp() 1079

Punto3f 68

〈Intervalo Definiciones〉 ≡

clase Intervalo {

public:

〈Intervalo Métodos Públicos 112〉

Float low, high;

};

Un intervalo puede ser inicializado con un único valor, que representa un único punto en la recta de números reales, o con dos valores que especifican un intervalo con anchura distinta de cero.

〈Intervalo Métodos Públicos〉 ≡ 112

Intervalo(Float v) : low(v), high(v) { }

Intervalo(Float v0, Float v1)

: low(std::min(v0, v1)), high(std::max(v0, v1)) { }

La clase también proporciona sobrecargas para las operaciones aritméticas básicas. Nótese que para la resta, el valor alto del segundo intervalo se resta del valor bajo del primero.3

〈Intervalo Métodos Públicos〉 + ≡ 112

Operador de intervalo + (const Intervalo &i) const {

retornar Intervalo(bajo + i.low, high + i.high);

}

Interval operator-(const Interval &i) const {

return Interval(low – i.alto, alto – i.bajo);

}

Para la multiplicación, los lados de cada intervalo que determinan los valores mínimos y máximos del intervalo resultante dependen de los signos de los respectivos valores. Multiplicar las distintas posibilidades y tomar el mínimo y el máximo global es más fácil que trabajar en cuáles usar y multiplicar éstos.

〈Intervalo Métodos Públicos〉 + ≡ 112

Intervalo operator*(const Intervalo &i) const {

devuelve Intervalo(std::min(std::min(low * i.bajo, alto * i.bajo),

std::min(bajo * i.alto, alto * i.alto)),

std::max(std::max(bajo * i.bajo, alto * i.bajo),

std::max(bajo * i.alto, alto * i.alto));

}

También hemos implementado las funciones Sin() y Cos() para Intervalos. Las implementaciones asumen que el intervalo dado está en , que es el caso de nuestro uso de estas funciones. Aquí sólo incluimos la implementación de Sin(); Cos() es bastante similar en su estructura básica.

Float 1062

Intervalo 112

Intervalo::alto 112

Intervalo::bajo 112

〈Intervalo Definiciones〉 + ≡

inline Intervalo Sin(const Intervalo &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);

}

Dada la maquinaria del intervalo, ahora podemos implementar la función IntervalFindZeros(), que encuentra los valores t de cualquier cruce de cero de la Ecuación (2.12) sobre el intervalo tInterval dado.

〈Intervalo Definiciones〉 + ≡

void IntervalFindZeros(Float c1, Float c2, Float c3, Float c4,

Float c5, Float theta, Intervalo tInterval, Float *zeros,

int *zeroCount, int depth = 8) {

〈Evalúa la derivada del movimiento en forma de intervalo, devolver si no hay ceros 113〉

if (depth> 0) {

〈Split tInterval and check both resulting intervals 114〉

} else {

〈Utiliza el método de Newton para refinar el cero 114〉

}

}

La función comienza calculando el rango del intervalo sobre tIntervalo. Si el rango no abarca cero, entonces no hay ceros de la función sobre tIntervalo y la función puede regresar.

〈Evalúa la derivada del movimiento en forma de intervalo, devolver si no hay ceros〉 ≡ 113

Rango del intervalo = Interval(c1) +

(Interval(c2) + Interval(c3) * tInterval) *

. Cos(Intervalo(2 * theta) * tIntervalo) +

(Intervalo(c4) + Intervalo(c5) * tIntervalo) *

Sin(Intervalo(2 * theta) * tIntervalo);

si (rango.low> 0. ∥ range.high <0. ∥ range.low == range.high)

return;

Si el rango del intervalo sí abarca el cero, entonces puede haber uno o más ceros en el intervalo tInterval, pero también es posible que en realidad no haya ninguno, ya que los límites del intervalo son conservadores pero no lo más ajustados posible. La función divide tIntervalo en dos partes y comprueba recursivamente los dos subintervalos. Al reducir el tamaño del dominio del intervalo generalmente se reduce la extensión del rango del intervalo, lo que puede permitirnos determinar que no hay ceros en uno o en los dos nuevos intervalos.

Float 1062

Intervalo 112

Intervalo::alto 112

Intervalo::low 112

Pi 1063

〈Dividir tInterval y comprobar ambos intervalos resultantes〉 ≡ 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);

Una vez que tenemos un intervalo estrecho donde el valor del intervalo de la función derivada del movimiento abarca el cero, la implementación cambia a unas cuantas iteraciones del método de Newton para encontrar el cero, comenzando en el punto medio del intervalo. El método de Newton requiere la derivada de la función; como estamos encontrando ceros de la función derivada del movimiento, ésta es la segunda derivada de la ecuación (2.11):

d2aM,ptdt2=c3,x+2θc4,x+c5,xtcos2θt+c5,x-2θc2,x+c3,xtsin2θt.

〈Usa el método de Newton para refinar el cero〉 ≡ 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);

si (fNewton == 0 ∥ fPrimeNewton == 0)

break;

tNewton = tNewton – fNewton / fPrimeNewton;

Deja un comentario