Evite trigonometria

Entrada


Parece-me que precisamos usar menos trigonometria em computação gráfica. Uma boa compreensão das projeções, reflexões e operações vetoriais (como no verdadeiro significado dos produtos escalares (pontos) e vetoriais (transversais) dos vetores) geralmente vem com um crescente sentimento de ansiedade ao usar a trigonometria. Mais precisamente, acredito que a trigonometria é boa para inserir dados no algoritmo (para o conceito de ângulos, esta é uma maneira intuitiva de medir a orientação), sinto que algo está errado quando vejo a trigonometria localizada nas profundezas de algum algoritmo de renderização 3D. Na verdade, acho que em algum lugar um gatinho morre quando a trigonometria se arrasta por lá. E não estou tão preocupado com velocidade ou precisão, mas com elegância conceitual, acho ... Agora vou explicar.

Em outros lugares, eu já discuti que produtos escalares e vetoriais de vetores contêm todas as informações necessárias para rotações, para essas duas operações "retangulares" - senos e cossenos de ângulos. Essa informação é equivalente a senos e cossenos em um número tão grande de lugares que parece que você pode simplesmente usar o produto de vetores e se livrar da trigonometria e dos ângulos. Na prática, você pode fazer isso permanecendo em vetores euclidianos comuns, sem trigonometria. Isso nos faz pensar: "Não estamos fazendo algo supérfluo?" Parece estar fazendo. Infelizmente, mesmo profissionais experientes tendem a abusar da trigonometria e tornam as coisas muito complexas, complicadas e não as mais concisas. E mesmo possivelmente "errado".

Vamos parar de tornar o artigo ainda mais abstrato. Vamos imaginar um dos casos de substituição de fórmulas trigonométricas por produtos vetoriais e ver o que acabei de falar.

Opção incorreta para girar um espaço ou objeto


Vamos ter uma função que calcula a matriz de rotação de um vetor em torno de um vetor normalizado na esquina . Em qualquer mecanismo 3D ou biblioteca matemática em tempo real, haverá uma dessas funções, que provavelmente será copiada cegamente de outro mecanismo, tutorial da Wikipedia ou do OpenGL ... (sim, neste momento você deve admitir e, dependendo do seu humor, é possível se preocupar com por causa disso).

A função será mais ou menos assim:

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

Imagine que você está vasculhando o interior de uma demo ou jogo, possivelmente finalizando algum tipo de módulo de animação, e precisa girar o objeto em uma determinada direção. Você deseja girá-lo para que um de seus eixos, digamos, um eixo coincidiu com um vetor específico digamos, tangente ao caminho da animação. Obviamente, você decide criar uma matriz que conterá transformações usando rotationAxisAngle() . Então, você primeiro precisará medir o ângulo entre o eixo seu objeto e o vetor de orientação desejado. Como você é um programador gráfico, sabe que isso pode ser feito com um produto escalar e, em seguida, extraindo o ângulo com acos() .

Além disso, você sabe que às vezes acosf() pode retornar valores estranhos se o produto escalar estiver fora do intervalo [-1; 1] e você decide alterar seu valor para que ele caia nesse intervalo ( aprox. Por pinça) (nesse ponto, você pode até culpar a precisão do seu computador, porque o comprimento do vetor normalizado não é exatamente 1). Nesse ponto, um gatinho morreu. Mas até você saber, você continua escrevendo seu código. Em seguida, você calcula o eixo de rotação e sabe que este é um produto vetorial de um vetor seu objeto e a direção escolhida , todos os pontos do seu objeto girarão em planos paralelos aos definidos por esses dois vetores, apenas no caso ... (o gatinho foi revivido e morto novamente). Como resultado, o código se parece com isso:

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

Para entender por que isso funciona, mas ainda erroneamente, abriremos todo o código rotationAxisAngle() e veremos o que realmente acontece:

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

Como você deve ter notado, estamos fazendo uma chamada acos bastante imprecisa e cara para cancelá-la imediatamente, calculando o cosseno do valor de retorno. E a primeira pergunta aparece: "por que não pular a cadeia de chamadas acos() ---> cos() e economizar tempo de CPU?" Além disso, isso não nos diz que estamos fazendo algo errado e muito complicado, e que algum princípio matemático simples chega até nós que se manifesta através da simplificação dessa expressão?

Você pode argumentar que a simplificação não pode ser feita, pois você precisará de um ângulo para calcular o seno. No entanto, isso não é verdade. Se você está familiarizado com o produto vetorial de vetores, sabe que, assim como o produto escalar contém cosseno, o vetor contém seno. A maioria dos programadores gráficos entende por que um produto escalar de vetores é necessário, mas nem todos entendem por que um produto vetorial é necessário (e use-o apenas para ler normais e eixos de rotação). Basicamente, o princípio matemático que nos ajuda a livrar-se do par cos / acos também nos diz que onde há um produto escalar, possivelmente existe um produto vetorial que relata a informação que falta (parte perpendicular, seno).

A maneira correta de girar um espaço ou objeto


Agora podemos extrair o seno do ângulo entre e apenas olhando o comprimento do produto vetorial ... - lembre-se de que e normalizado! E isso significa que podemos (devemos !!) reescrever a função desta maneira:

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

e certifique-se de que nossa nova função de construção da matriz de rotação, rotationAxisCosSin() , não calcule senos e cossenos em qualquer lugar, mas os tome como argumentos:

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

Há mais uma coisa que pode ser feita para se livrar de normalizações e raízes quadradas - encapsular toda a lógica em uma nova função e passar 1/si para a matriz:

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

Mais tarde, Zoltan Vrana notou que k pode ser simplificado para k = 1/(1+c) , que não apenas parece matematicamente mais elegante, mas também move dois recursos para ke, portanto, toda a função ( e paralelo) entra em um (quando e coincidir neste caso, não há rotação clara). O código final é mais ou menos assim:

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

Não apenas nos livramos de três funções trigonométricas e nos livramos da pinça feia (e da normalização!), Mas também simplificamos conceitualmente nossa matemática 3D. Nenhuma função transcendental; apenas vetores são usados ​​aqui. Vetores criam matrizes que modificam outros vetores. E isso é importante, porque quanto menos trigonometria em seu mecanismo 3D, não apenas mais rápido e mais claro, mas também, primeiro, matematicamente mais elegante (mais correto!).

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


All Articles