opengl tutorial




Trasformando correttamente un nodo rispetto a uno spazio specificato? (2)

Attualmente sto lavorando con i nodi in un grafico di scena gerarchico e ho difficoltà a tradurre / ruotare correttamente un nodo relativo a uno specifico spazio di trasformazione (ad esempio un nodo genitore).

Come faccio a tradurre / ruotare correttamente un nodo relativo al suo nodo genitore in un grafico di scena?

Il problema

Si consideri il seguente diagramma molecolare dell'acqua (senza le linee di collegamento) per la struttura genitore / figlio dei nodi scena, con l'atomo O xygen come nodo genitore e gli atomi 2 H ydrogeno come nodi figlio.

Problema di traduzione

Se prendi l'atomo O xygen genitore e traduci la struttura, ti aspetti che i figli H ydrogen seguano e rimangano nella stessa posizione relativa rispetto al genitore. Se invece prendi un atomo di bambini H e lo traduci, solo il bambino ne risentirebbe. Questo è generalmente come funziona attualmente. Quando gli atomi di O vengono tradotti, gli atomi di H si spostano automaticamente con esso, come previsto da un grafico gerarchico.

Tuttavia , quando traducono il genitore, anche i bambini finiscono per accumulare una traduzione aggiuntiva , che essenzialmente fa sì che i bambini "traducano due volte" nella stessa direzione e si allontanino dal genitore invece di stare alla stessa distanza relativa.

Problema di rotazione

Se prendi il nodo O genitore e lo fai ruotare, ti aspetti che ruoti anche i nodi H figli, ma in un'orbita, perché la rotazione viene eseguita dal genitore. Funziona come previsto.

Tuttavia , se prendi un nodo H figlio e gli dici di ruotare rispetto al suo genitore , mi aspettavo che il bambino finisse per orbitare attorno al genitore nello stesso modo, ma questo non accade. Invece, il bambino ruota sul proprio asse ad una velocità maggiore (ad esempio, il doppio della velocità rispetto alla rotazione rispetto al proprio spazio locale) nella sua posizione corrente.

Spero davvero che questa descrizione sia abbastanza giusta, ma fammi sapere se non lo è e chiarirò se necessario.

La matematica

Sto usando le matrici 4x4 colonna-principale (cioè Matrix4 ) e i vettori delle colonne (ad esempio Vector3 , Vector4 ).

La logica errata di seguito è la più vicina a cui sono giunto per il comportamento corretto. Si noti che ho scelto di utilizzare una sintassi simile a Java, con l'overloading dell'operatore per semplificare la lettura qui. Ho provato cose diverse quando pensavo di aver capito, ma davvero non l'avevo fatto.

Logica di traduzione attuale

translate(Vector3 tv /* translation vector */, TransformSpace relativeTo):
    switch (relativeTo):
        case LOCAL:
            localTranslation = localTranslation * TranslationMatrix4(tv);
            break;
        case PARENT:
            if parentNode != null:
                localTranslation = parentNode.worldTranslation * localTranslation * TranslationMatrix4(tv);
            else:
                localTranslation = localTranslation * TranslationMatrix4(tv);
            break;
        case WORLD:
            localTranslation = localTranslation * TranslationMatrix4(tv);
            break;

Logica di rotazione attuale

rotate(Angle angle, Vector3 axis, TransformSpace relativeTo):
    switch (relativeTo):
        case LOCAL:
            localRotation = localRotation * RotationMatrix4(angle, axis);
            break;
        case PARENT:
            if parentNode != null:
                localRotation = parentNode.worldRotation * localRotation * RotationMatrix4(angle, axis);
            else:
                localRotation = localRotation * RotationMatrix4(angle, axis);
            break;
        case WORLD:
            localRotation = localRotation * RotationMatrix4(angle, axis);
            break;

Calcolo delle trasformazioni dello spazio mondiale

Per motivi di completezza, le trasformazioni del mondo per this nodo sono calcolate come segue:

if parentNode != null:
    worldTranslation = parent.worldTranslation * localTranslation;
    worldRotation    = parent.worldRotation    * localRotation;
    worldScale       = parent.worldScale       * localScale;
else:
    worldTranslation = localTranslation;
    worldRotation    = localRotation;
    worldScale       = localScale;

Inoltre, la trasformazione completa / accumulata del nodo per this è:

Matrix4 fullTransform():
    Matrix4 localXform = worldTranslation * worldRotation * worldScale;

    if parentNode != null:
        return parent.fullTransform * localXform;

    return localXform;

Quando viene richiesta la trasformazione di un nodo per l'uniforme shader OpenGL, viene utilizzata la matrice fullTransform .


worldTranslation = parentNode.worldTranslation * localTranslation;
worldRotation    = parentNode.worldRotation    * localRotation;
worldScale       = parentNode.worldScale       * localScale;

Non è così che funziona l'accumulazione delle trasformazioni successive. Ed è ovvio perché no se ci pensi.

Diciamo che hai due nodi: un genitore e un bambino. Il genitore ha una rotazione locale di 90 gradi in senso antiorario attorno all'asse Z. Il bambino ha un offset di +5 sull'asse X. Beh, una rotazione in senso antiorario dovrebbe far sì che abbia un +5 nell'asse Y, sì (supponendo un sistema di coordinate destrorso)?

Ma non è così. localTranslation non è mai interessato da alcuna forma di rotazione.

Questo è vero per tutte le tue trasformazioni. Le traduzioni sono influenzate solo dalle traduzioni, non da scale o rotazioni. Le rotazioni non sono influenzate dalle traduzioni. Eccetera.

Questo è ciò che il tuo codice dice di fare, e non è come lo si dovrebbe fare.

Mantenere decomposti i componenti delle tue matrici è una buona idea. Cioè, avere componenti separati di traduzione, rotazione e scala (TRS) è una buona idea. Rende più semplice applicare successive trasformazioni locali nell'ordine corretto.

Ora, mantenere i componenti come matrici è sbagliato, perché non ha senso e spreca tempo e spazio senza una vera ragione. Una traduzione è solo un vec3 , e non c'è nulla da guadagnare memorizzando 13 altri componenti con esso. Quando si accumulano traduzioni localmente, basta aggiungerle.

Tuttavia, nel momento in cui è necessario accumulare la matrice finale per un nodo, è necessario convertire ciascuna scomposizione TRS nella propria matrice locale, quindi trasformarla nella trasformazione generale del genitore, non nelle singole componenti TRS del genitore. Cioè, è necessario comporre le trasformazioni separate localmente, quindi moltiplicarle con la matrice di trasformazione genitore. Nello pseudo-codice:

function AccumRotation(parentTM)
  local localMatrix = TranslationMat(localTranslation) * RotationMat(localRotation) * ScaleMat(localScale)
  local fullMatrix = parentTM * localMatrix

  for each child
    child.AccumRotation(fullMatrix)
  end
end

Ogni genitore passa la propria rotazione accumulata al bambino. Al nodo radice viene fornita una matrice di identità.

Ora, la decomposizione TRS è perfetta, ma funziona solo quando si tratta di trasformazioni locali . Cioè, le trasformazioni relative al genitore. Se si desidera ruotare un oggetto nel suo spazio locale, si applica un quaternione al suo orientamento.

Ma eseguire una trasformazione in uno spazio non locale è una storia completamente diversa. Se si desidera, ad esempio, applicare una traduzione nello spazio del mondo a un oggetto che ha alcune serie arbitrarie di trasformazioni applicate ad esso ... è un compito non banale. In realtà, è un compito facile: si calcola la matrice spazio-mondo dell'oggetto, quindi si applica una matrice di traduzione alla sinistra di quella, quindi si utilizza l'inverso della matrice spazio-mondo del genitore per calcolare la trasformazione relativa al genitore.

function TranslateWorld(transVec)
  local parentMat = this->parent ? this->parent.ComputeTransform() : IdentityMatrix
  local localMat = this->ComputeLocalTransform()
  local offsetMat = TranslationMat(localTranslation)
  local myMat = parentMat.Inverse() * offsetMat * parentMat * localMat
end

Il significato della cosa P -1 O P è in realtà un costrutto comune. Significa trasformare la trasformazione generale O nello spazio di P Quindi, trasforma un mondo sfalsato nello spazio della matrice del genitore. Applichiamo quindi questo alla nostra trasformazione locale.

myMat ora contiene una matrice di trasformazione che, moltiplicata per le trasformazioni del genitore, applicherà transVec come se fosse nello spazio del mondo. Questo è quello che volevi.

Il problema è che myMat è una matrice , non una decomposizione TRS. Come si torna a una decomposizione TRS? Beh ... questo richiede una matematica matematica non banale. Richiede di fare qualcosa chiamato Decomposizione Singolare Valore . E anche dopo aver implementato la brutta matematica, SVD può fallire . È possibile avere una matrice non scomponibile.

In un sistema di diagrammi di scena che ho scritto, ho creato una classe speciale che era effettivamente un'unione tra una scomposizione TRS e la matrice che rappresenta. Potresti chiedere se è stato decomposto, e se lo fosse, potresti modificare i componenti TRS. Ma una volta provato ad assegnare un valore di matrice 4x4 direttamente ad esso, esso diventa una matrice composta e non è più possibile applicare trasformazioni locali decomposte. Non ho mai nemmeno provato a implementare SVD.

Oh, potresti accumulare matrici in esso. Ma l'accumulo successivo di trasformazioni arbitrarie non produrrà lo stesso risultato delle modifiche componenti decomposte. Se vuoi influenzare la rotazione senza influenzare le traduzioni precedenti, puoi farlo solo se la classe è in uno stato decomposto.

In ogni caso, il tuo codice ha alcune idee corrette, ma anche alcune molto errate. È necessario decidere quanto sia importante avere una scomposizione TRS e quanto sia importante essere in grado di applicare una trasformazione non locale.


Ho trovato la risposta di Nicol Bolas un po 'utile, anche se c'erano ancora alcuni dettagli su cui non ero così chiaro. Ma quella risposta mi ha aiutato a vedere la natura non banale del problema su cui stavo lavorando, quindi ho deciso di semplificare le cose.

Una soluzione più semplice - Sempre nello Spazio Genitore

Ho rimosso Node.TransformSpace per semplificare il problema. Tutte le trasformazioni vengono ora applicate in relazione allo spazio del Node padre e le cose funzionano come previsto. Sono ora disponibili anche le modifiche alla struttura dei dati che intendevo eseguire dopo aver implementato le cose (ad es. Sostituire le matrici di traduzione / ridimensionamento locali per vettori semplici).

Segue un riepilogo della matematica aggiornata.

Traduzione aggiornata

La posizione di un Node è ora rappresentata da un oggetto Vector3 , con Matrix4 costruito su richiesta (vedere più avanti).

void translate(Vector3 tv /*, TransformSpace relativeTo */):
    localPosition += tv;

Rotazione aggiornata

Le rotazioni sono ora contenute in una Matrix3 , cioè una matrice 3x3.

void rotate(Angle angle, Vector3 axis /*, TransformSpace relativeTo */):
    localRotation *= RotationMatrix3(angle, axis);

Ho ancora in programma di esaminare i quaternioni dopo, dopo aver verificato che le mie conversioni di matrice <=> sono corrette.

Ridimensionamento aggiornato

Come la posizione di un Node , il ridimensionamento ora è anche un oggetto Vector3 :

void scale(Vector3 sv):
    localScale *= sv;

Calcoli delle trasformazioni locali / mondiali aggiornati

Il seguente aggiornamento del mondo di un Node trasforma in relazione al Node principale, se presente. Un problema qui è stato risolto rimuovendo una concatenazione non necessaria alla trasformazione completa del genitore (vedi post originale).

void updateTransforms():
    if parentNode != null:
         worldRotation = parent.worldRotation * localRotation;
         worldScale    = parent.worldScale    * localScale;
         worldPosition = parent.worldPosition + parent.worldRotation * (parent.worldScale * localPosition);
    else:
        derivedPosition = relativePosition;
        derivedRotation = relativeRotation;
        derivedScale    = relativeScale;

    Matrix4 t, r, s;

    // cache local/world transforms
    t = TranslationMatrix4(localPosition);
    r = RotationMatrix4(localRotation);
    s = ScalingMatrix4(localScale);
    localTransform = t * r * s;

    t = TranslationMatrix4(worldPosition);
    r = RotationMatrix4(worldRotation);
    s = ScalingMatrix4(worldScale);
    worldTransform = t * r * s;