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.
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:
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:
Ebből mátrix egyenletet csinálva:
É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. 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ű.
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. 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.
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:
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:
É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:
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:
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.
Höfö:
|