64. fejezet - 3D objektumok mozgatása egérrel


Ez megint egy olyan cikk, amihez nem lesz kód, ezért igyekszem matematikailag levezetni amit csak lehet. A feladat az, hogy egy 3D objektumot (ami jelen esetben egy vágósík) az egérrel (illetve tableten ujjal) lehessen húzogatni de úgy, hogy a mozgás pontosan kövesse az egeret.

A megoldás két darab metódus implementálására szorítkozik. Az onmousedown() egyszer hívódik meg, ekkor az állapot beáll arra, hogy most cibálod az objektumot. Meghatározandó egy 3D pont, amit éppen tapicskolsz. Az onmousemove()-ban hasonló a feladat, és kiszámolandó az objektum új pozíciója. Ha felengedted az egeret, akkor az állapot visszaáll üresre.

Kiemelném, hogy bár én itt egy síklapot ráncigálok, tetszőleges objektumra ugyanígy lehet eljárni, amennyiben meg tudod határozni egy sugárral való metszetét.

Sugár-objektum metszet

Az első lépés könnyű, hiszen csak meg kell határozni egy sugarat az aktuális kamerából, és kideríteni a metszetét az objektummal.

CODE
void GetViewRay( qVector3& start, qVector3& dir, const qMatrix4& viewproj, const qVector2& loc) { const qViewport& viewport = gfx->GetViewport(); qMatrix4 unproj; qVector4 worldpos; qVector4 screenpos(loc.x, loc.y, 0, 1); qMatrix4::Inverse(unproj, viewproj); // pont a közeli vágósíkon viewport.Unproject(worldpos, screenpos, unproj); start = worldpos; // egy másik pont screenpos.z = 0.5f; viewport.Unproject(worldpos, screenpos, unproj); dir = worldpos - start; qVector3::Normalize(dir, dir); }

Megjegyzem, hogy vektorokat nem lehet vetíteni, tehát két levetített pontból kell meghatározni. Az Unproject() függvényt nem kell nagyon magyarázni, mert csak átkonvertálja a pontot a [-1, 1] x [1, -1] tartományba, beszorozza a mátrixxal és leoszt w-vel.

Jelen esetben az objektum egy síkdarab (mint egy papírlap), amihez könnyű meghatározni a metszetet a síkegyenletből, illetve a sík tangent frame-jéből. Ugyanis, vegyük az (a, b, c, d) síkot, és az (s, r) sugarat, ekkor:

n := (a, b, c)

<n, s + tr> + d = 0
<n, s> + t<n, r> + d = 0

t = -(d + <n, s>) / <n, r>

Ez azt jelenti, hogy a sugár egy pontja rajta van a síkon, aminek persze előfeltétele, hogy a sugár nem párhuzamos a síkkal, ezt höfö lekezelni.

A másik meggondolás az, hogy ha a síklapnak ismered a koordinátáit, akkor abból meg lehet kapni egy u = v1 - v0 és v = v3 - v0 vektort. Márpedig az, hogy egy x pont rajta van a síklapon azt jelenti, hogy felírható ilyen alakban:

x = v0 + λu + μv     (λ, μ ∈ [0, 1])

Ebből mátrix egyenletet csinálva:

b := (λ, μ, 0, 1)T

M :=
 [ ux vx nx v0x ]
 [ uy vy ny v0y ]
 [ uz vz nz v0z ]
 [ 0 0 0 1 ]

Mb = x    ->    b = M-1x

És csak le kell ellenőrizni, hogy a kapott értékek a [0, 1] intervallumban vannak-e.

CODE
qVector3 trackpos; float t1; void onmousedown(const qMouseState& mstate) { const InteractiveCutPlane& cutplane = cutplanes[selectedplane]; qMatrix4 viewproj = ...; qMatrix4 inv, basis = ...; qVector3 raystart, raydir; qVector3 p; GetViewRay(raystart, raydir, viewproj, qVector2(mstate.X, mstate.Y)); // sík-sugár metszet if( qPlane::RayIntersect(t1, cutplane.plane, raystart, raydir) ) { // λ és μ kiszámolása qMatrix4::Inverse(inv, basis, 0); p = raystart + t1 * raydir; qVector3::Transform(p, p, inv); // ha a síkon belül van if( p.x >= 0 && p.x <= 1 && p.y >= 0 && p.y <= 1 ) { trackpos = p; } } }

Na de a probléma: mozgatáskor ebből a kapott pontból, illetve egy másik (egyébként ismeretlen) pontból kéne meghatározni, hogy a sík hol lesz. Hogy ne legyen olyan könnyű a dolog, a síkot csak a normálvektora mentén lehet mozgatni, azaz az egér pozíciójának köze nincs a most kiszámolt ponthoz!


0. megoldás: ray paraméter becslése

Ez egy hihetetlenül triviális megoldás: azt feltételezzük, hogy a keresett másik 3D pont ugyanazzal a t1 paraméterrel jön ki, mint előbb. Ez azért merül fel egyáltalán, mert annyit azért tudunk, hogy az új pontot a mozgatás vektora mentén kell keresni.

CODE
void onmousemove(const qMouseState& mstate) { InteractiveCutPlane& cutplane = cutplanes[selectedplane]; qVector3 raystart, raydir; GetViewRay(raystart, raydir, viewproj, qVector2(mstate.X, mstate.Y)); cutplane.plane.d = 0; cutplane.plane.d = -qPlane::Distance(cutplane.plane, raystart + t1 * raydir); }

Mi is történik itt? Azt mondom, hogy az új pont ugyanolyan messze van a közeli vágósíktól, mint az előbb kiszámolt. Mivel a mozgatás meg van szorítva a sík normálvektorára, az új pontot még rá kellene vetíteni, de vegyük észre, hogy a levetített pont is ugyanolyan messze lenne a síktól, így ezt meg lehet spórolni.

Picture

A feltételezés gyakorlatilag soha nem igaz (kivéve párhuzamos vetítéssel), viszont olcsó és többnyire használható. Azaz ha a mozgás iránya nem merőleges az XZ síkra, akkor nem követi rendesen az ujjad (és ez is csak addig igaz amíg "normális" kamerád van).


1. megoldás: segéd síkok

Ahogy elnéztem az 3ds Max-ot, az is valami hasonló dolgot csinál. Mivel az egér tetszőlegesen elkalandozhat a mozgatásra kijelölt egyenestől, érdemesebb meghatározni egy síkot, ami tartalmazza az egyenest. A probléma csak az, hogy ez a bizonyos sík nagyon nem egyértelmű.

A gondolatmenet a következő: az aktuális mozgásirányra merőleges síkok közül válasszuk ki a legjobbat. A legjobb legyen az, ami leginkább a kamera felé néz. Vegyük, most az (a, b, c, d) síkot, ekkor az alábbi három lehetőség jön szóba:

p1 = (-b, a, c, d)
p2 = (-c, b, a, d)
p3 = (a, -c, b, d)

Valójában van még három lehetőség, de azok a probléma szempontjából ugyanezek (másik irányba néz). Ebből a háromból viszont csak kettő merőleges az eredeti síkra.

CODE
bool GetGuidePlane(qPlane& out, const qPlane& plane, const qVector3& fwd) { // 3ds Max féle megoldás: kiválasztani a legjobb merőleges síkot qPlane p1(-plane.b, plane.a, plane.c, plane.d); qPlane p2(-plane.c, plane.b, plane.a, plane.d); qPlane p3(plane.a, -plane.c, plane.b, plane.d); float u, v, maxv = 0; // teszteli, hogy merőleges-e, és mennyire néz a kamera felé u = fabs(p1.a * plane.a + p1.b * plane.b + p1.c * plane.c); v = fabs(p1.a * fwd.x + p1.b * fwd.y + p1.c * fwd.z); if( u < 0.1f && v > maxv ) { maxv = v; out = p1; } // TODO: többire ugyanígy return (maxv > 0.01f); }

A használat elég nyilvánvaló: a mozgatáshoz szükséges második pont a sugár és a kiválasztott segéd sík metszete lesz, szintén rávetítve még az eredeti sík normáljára.

Picture 2

Viszont van olyan eset, amikor mindegyik sík rossz, például ha a kamera az ábrán szereplő síkot majdnem felülről nézi. Ekkor mindkét segéd sík nagyon éles szögben látszik és nehézkes a mozgatás, sőt nem is mindig egyértelmű hogy merre kell mozgatni az egeret.


2. megoldás: a segéd sík lehetőleg a kamera felé fordul

Ez alatt azt kell érteni, hogy amennyire csak tud, a kamera felé fordul (a mozgásirány tengelye körül foroghat). Ez azért jó, mert nem kell válogatni a síkok között: a kamera nézési irányát rávetítem az aktuális síkra, és az lesz a segéd sík normálvektora.

Picture 3

Ez az előbbi problémát nem oldja meg, és bizonyos esetekben a segéd sík elég idióta és természetellenes irányokba kell mozgatni az egeret, hogy az történjen amit akarsz (pl. ha az objektumot az X tengely mentén akarod ráncigálni).


3. megoldás: determinánsos módszer

Ez a lusta programozók módszere: felejtsük el a problémát. A baj ugye az, hogy az egér elkalandozott a mozgatás egyenesétől. Semmi gond, vetítsük le rá screen space-ben, ezzel a feladat máris sokkal egyszerűbb, hiszen az ebből kapott sugárra:

trackpos + λn = s + μr

Hát és akkor mivan, van két ismeretlened, ezzel mihez kezdesz? Felhívnám a figyelmet arra, hogy a fenti egyenlet valójában három darab egyenlet (x, y és z koordinátára). Tehát az egyenletrendszer túlhatározott, ami azért valamivel jobb. Vadul fel is írom mátrix alakban:
b := trackpos - s
x := (μ, -λ, 0, 1)

M :=
 [ rx nx 0 0 ]
 [ ry ny 0 0 ]
 [ rz nz 1 0 ]
 [ 0 0 0 1 ]

Mx = b

És itt most álljunk meg egy pillanatra, ez ugyanis nem egy szokványos mátrix. Tudjuk, hogy inverz akkor létezik, ha a determináns nem nulla. Ennek a mátrixnak a determinánsa könnyen kiszámolható a harmadik oszlop szerint kifejtve:

det(M) = nxry - rxny

Ami bizony vidáman lehet nulla igen sok esetben. De mi tűnik fel? A harmadik sornak köze nincs a determinánshoz, ez meg miféle dolog? Azért kéremszépen, mert ennek az egyenletrendszernek nem ez az egyetlen mátrix alakja. Fel is írom a másik kettőt:

M :=
 [ rx nx 1 0 ]
 [ ry ny 0 0 ]
 [ rz nz 0 0 ]
 [ 0 0 0 1 ]

     M :=
 [ rx nx 0 0 ]
 [ ry ny 1 0 ]
 [ rz nz 0 0 ]
 [ 0 0 0 1 ]

Így mindjárt jobb a helyzet: van három esély arra, hogy a determináns ne legyen nulla. Ha mindhárom esetben nulla, akkor nincs megoldás, ez akkor fordul elő, amikor pontosan merőlegesen nézel a tapizott síkra.

CODE
void onmousemove(const qMouseState& mstate) { InteractiveCutPlane& cutplane = cutplanes[selectedplane]; const qViewport& viewport = game->Graphics->GetViewport(); // 1. lépés: kiszámolni a mozgatás irányát screen spaceben qVector4 s1(trackpos, 1); qVector4 s2(trackpos + cutplane.plane.AsNormal(), 1); qVector2 linedir; viewport.Project(s1, s1, viewproj); viewport.Project(s2, s2, viewproj); linedir.x = s2.x - s1.x; linedir.y = s2.y - s1.y; // 2. lépés: egér pozíciót rávetíteni, majd innen indítani a sugarat qMatrix4 viewproj = ...; qMatrix4 unproj; qVector3 raystart, raydir; qVector2 hit, t; Quadron::pointDistanceToLine(t, qVector2(mstate.X, mstate.Y), s1, linedir); hit.x = s1.x + t.y * linedir.x; hit.y = s1.y + t.y * linedir.y; qMatrix4::Inverse(unproj, viewproj); GetViewRay(raystart, raydir, unproj, hit); // 3. lépés: determinánsos móka qVector3 n(cutplane.plane); qVector4 x, b(trackpos - raystart, 1); qMatrix4 inv, m( raydir.x, raydir.y, raydir.z, 0, n.x, n.y, n.z, 0, 0, 0, 1, 0, 0, 0, 0, 1); float det = (m._11 * m._22 - m._12 * m._21); if( fabs(det) < 1e-5f ) { m._33 = 0; m._32 = 1; det = (m._11 * m._23 - m._13 * m._21); if( fabs(det) < 1e-5f ) { m._32 = 0; m._31 = 1; det = (m._12 * m._23 - m._13 * m._22); if( fabs(det) < 1e-5f ) return; } } qMatrix4::Inverse(inv, m); qVector4::Transform(x, b, inv); hitpos = raystart + x.x * raydir; }

A megoldás tök jó, a működés viszont érdekes. Tipikusan akkor mozog jól az objektum, ha a screen space-beli normálvektor mentén mozgatod. A baj az, hogy ezt egy átlag user marhára nem fogja fel.


Summarum

Na de akkor melyik megoldás a legjobb? Őszinte leszek: halvány fingom sincs. Mint mondtam, a 3ds Max az 1. megoldást használja, a BIMx a 0. megoldást (mivel ott csak fel/le lehet mozgatni), én az engine-ben a 3. megoldást hagytam meg, kiegészítve egy nagyon aranyos ugráló kék nyíllal, ami mutatja a usernek, hogy merre kell húzni az ujját.

Picture 3 Picture 4
Kéne még alá valami bumszli...


Höfö:
  • Implementáld le a 4., 5. és 6. megoldást!
  • Küldd el nekem...

back to homepage

Valid HTML 4.01 Transitional Valid CSS!