Navigation▲
I. Cliquer sur un objet avec une bibliothèque physique▲
Dans ce tutoriel, nous allons voir la méthode « recommandée » pour cliquer sur un objet dans un moteur de jeu classique - ce qui peut ne pas être votre cas.
L'idée est que le moteur de jeu devra intégrer un moteur physique dans tous les cas et tous les moteurs physiques ont des fonctions pour obtenir l'intersection d'un rayon avec la scène. En plus, ces fonctions sont certainement mieux optimisées que ce que vous pouvez obtenir vous-même : tous les moteurs physiques utilisent des structures de partitionnement de l'espace évitant de tester les intersections avec les objets qui ne sont pas dans la même région.
Dans ce tutoriel, nous allons utiliser le moteur Bullet Physics Engine, mais les concepts sont identiques pour n'importe quel autre : PhysX, HavoK, etc.
II. Intégration de Bullet▲
De nombreux tutoriels expliquent comment intégrer Bullet ; en particulier, le wiki de Bullet est très bien fait.
// Initialise Bullet. Cela suit exactement http://bulletphysics.org/mediawiki-1.5.8/index.php/Hello_World,
// même si nous n'utiliserons pas la plupart de tout cela.
// Construit la broadphase
btBroadphaseInterface*
broadphase =
new
btDbvtBroadphase();
// Initialise la configuration de collision et le dispatcher
btDefaultCollisionConfiguration*
collisionConfiguration =
new
btDefaultCollisionConfiguration();
btCollisionDispatcher*
dispatcher =
new
btCollisionDispatcher(collisionConfiguration);
// Le solveur physique
btSequentialImpulseConstraintSolver*
solver =
new
btSequentialImpulseConstraintSolver;
// Le monde
btDiscreteDynamicsWorld*
dynamicsWorld =
new
btDiscreteDynamicsWorld(dispatcher,broadphase,solver,collisionConfiguration);
dynamicsWorld->
setGravity(btVector3(0
,-
9.81
f,0
));
Chaque objet doit avoir une forme de collision (Collision Shape). Même si celle-ci peut être le modèle en lui-même, cela est généralement une mauvaise idée pour les performances. À la place, on utilise habituellement des formes plus simples telles que les boîtes, les sphères ou les capsules. Voici quelques formes de collision. De gauche à droite : sphère, cube, convex hull du modèle, modèle d'origine. Les sphères sont moins précises que le modèle, mais bien plus rapides à tester.
Dans cet exemple, tous les modèles utilisent la même boîte :
btCollisionShape*
boxCollisionShape =
new
btBoxShape(btVector3(1.0
f, 1.0
f, 1.0
f));
Les moteurs physiques ne connaissent rien sur OpenGL ; et en réalité, ceux-ci peuvent s'exécuter sans aucune visualisation 3D. Donc vous ne pouvez pas directement donner votre VBO à Bullet. À la place, vous devez ajouter un corps solide (Rigid Body) dans la simulation.
btDefaultMotionState*
motionstate =
new
btDefaultMotionState(btTransform(
btQuaternion(orientations[i].x, orientations[i].y, orientations[i].z, orientations[i].w),
btVector3(positions[i].x, positions[i].y, positions[i].z)
));
btRigidBody::
btRigidBodyConstructionInfo rigidBodyCI(
0
, // masse, en kg. 0 -> objet statique, ne bougera jamais.
motionstate,
boxCollisionShape, // forme de collision du corps
btVector3(0
,0
,0
) // inertie locale
);
btRigidBody *
rigidBody =
new
btRigidBody(rigidBodyCI);
dynamicsWorld->
addRigidBody(rigidBody);
Notez que le corps rigide utilise une forme de collision pour définir sa forme.
Nous gardons aussi une trace du corps rigide, mais comme il est indiqué dans le commentaire, un vrai moteur aura d'une quelconque façon une classe MonGameObject avec la position, l'orientation, le modèle OpenGL et un pointeur vers le corps rigide.
rigidbodies.push_back(rigidBody);
// Petite astuce : garder l'indice du modèle « i » dans le pointer pour l'utilisateur de Bullet.
// Il sera utilisé pour connaître quel objet a été cliqué.
// Un vrai programme passera sûrement un « MonPointeurGameObject » à la place.
rigidBody->
setUserPointer((void
*
)i);
En d'autres mots : veuillez ne pas utiliser le code ci-dessus dans la vraie vie ! Ce n'est que pour les besoins de la démonstration.
III. Lancer de rayon▲
III-A. Trouver la position du rayon▲
Premièrement, nous devons trouver un rayon qui commence à la position de la caméra et va « à travers la souris ». Cela est fait dans la fonction ScreenPosToWorldRay().
Premièrement, nous trouvons la position de départ et de fin du rayon dans les coordonnées normalisées du périphérique. Nous le faisons dans cet espace, car c'est très simple :
// Les positions de départ et de fin du rayon, dans les coordonnées normalisées du périphérique (avez-vous lu le quatrième tutoriel ?)
glm::
vec4 lRayStart_NDC(
((float
)mouseX/
(float
)screenWidth -
0.5
f) *
2.0
f, // [0,1024] -> [-1,1]
((float
)mouseY/
(float
)screenHeight -
0.5
f) *
2.0
f, // [0, 768] -> [-1,1]
-
1.0
, // Le plan auprès correspond à Z=-1 dans l'espace de coordonnées normalisée du périphérique
1.0
f
);
glm::
vec4 lRayEnd_NDC(
((float
)mouseX/
(float
)screenWidth -
0.5
f) *
2.0
f,
((float
)mouseY/
(float
)screenHeight -
0.5
f) *
2.0
f,
0.0
,
1.0
f
);
Pour comprendre ce code, regardons une nouvelle fois à cette image du quatrième tutoriel :
L'espace de coordonnées normalisé du périphérique est un cube 2 x 2 x 2, centré sur l'origine, donc dans cet espace, le rayon « passant par la souris » n'est qu'une ligne droite, perpendiculaire au plan proche ! Cela rend IRayStart_NDC et IEndStart_NDC facile à calculer.
Maintenant, nous n'avons qu'à appliquer la transformation inverse :
// La matrice de projection va de l'espace caméra à l'espace de coordonnées normalisé du périphérique.
// Donc inverse(ProjectionMatrix) va de l'espace de coordonnées normalisé du périphérique à l'espace caméra.
glm::
mat4 InverseProjectionMatrix =
glm::
inverse(ProjectionMatrix);
// La matrice de vue va de l'espace monde à l'espace caméra.
// Donc inverse(ViewMatrix) va de l'espace caméra vers l'espace monde.
glm::
mat4 InverseViewMatrix =
glm::
inverse(ViewMatrix);
glm::
vec4 lRayStart_camera =
InverseProjectionMatrix *
lRayStart_NDC; lRayStart_camera/=
lRayStart_camera.w;
glm::
vec4 lRayStart_world =
InverseViewMatrix *
lRayStart_camera; lRayStart_world /=
lRayStart_world .w;
glm::
vec4 lRayEnd_camera =
InverseProjectionMatrix *
lRayEnd_NDC; lRayEnd_camera /=
lRayEnd_camera .w;
glm::
vec4 lRayEnd_world =
InverseViewMatrix *
lRayEnd_camera; lRayEnd_world /=
lRayEnd_world .w;
// Méthode plus rapide (qu'une seule inversion)
//glm::mat4 M = glm::inverse(ProjectionMatrix * ViewMatrix);
//glm::vec4 lRayStart_world = M * lRayStart_NDC; lRayStart_world/=lRayStart_world.w;
//glm::vec4 lRayEnd_world = M * lRayEnd_NDC ; lRayEnd_world /=lRayEnd_world.w;
Avec IRayStart_worldspace et IRayEnd_worldspace, la direction du rayon (dans l'espace monde) est facile à calculer :
glm::
vec3 lRayDir_world(lRayEnd_world -
lRayStart_world);
lRayDir_world =
glm::
normalize(lRayDir_world);
III-B. Utiliser RayTest()▲
Le lancer de rayon est très simple, aucun besoin de commentaire :
out_direction =
out_direction*
1000.0
f;
btCollisionWorld::
ClosestRayResultCallback RayCallback(
btVector3(out_origin.x, out_origin.y, out_origin.z),
btVector3(out_direction.x, out_direction.y, out_direction.z)
);
dynamicsWorld->
rayTest(
btVector3(out_origin.x, out_origin.y, out_origin.z),
btVector3(out_direction.x, out_direction.y, out_direction.z),
RayCallback
);
if
(RayCallback.hasHit()) {
std::
ostringstream oss;
oss <<
"mesh "
<<
(int
)RayCallback.m_collisionObject->
getUserPointer();
message =
oss.str();
}
else
{
message =
"background"
;
}
La seule chose est que pour une raison étrange, vous devez définir la position de départ et de fin du rayon, deux fois.
C'est tout, vous savez comment implémenter le clic sur les objets dans Bullet !
IV. Avantages et inconvénients▲
-
Avantages :
- très simple lorsque vous avez déjà un moteur physique ;
- rapide ;
- n'impacte pas les performances OpenGL.
-
Inconvénients :
- certainement pas la bonne solution si vous n'avez pas besoin de simulation physique ou d'un moteur de collision.
V. Remarques finales▲
Tous les moteurs physiques possèdent une vue de débogage. Le code d'exemple montre comment l'activer avec Bullet. Vous obtiendrez une représentation de ce que Bullet sait sur votre scène, ce qui est vraiment très utile pour déboguer les problèmes liés à la physique, notamment pour être sûr que le « monde réel » est consistant avec le « monde physique ».
La boîte verte est la forme de collision, à la même position et orientation que le modèle. La boîte rouge est la boîte englobante alignée sur les axes (AABB), qui est utilisée pour un rapide test de réjection : si le rayon ne touche pas le AABB (très facile à calculer), alors il ne touchera pas la forme de collision. Finalement, vous pouvez voir les axes de l'objet en bleu et rouge (regardez au nez et à l'oreille). Pratique !
VI. Remerciements▲
Cet article est une traduction autorisée dont le texte original peut être trouvé sur opengl-tutorial.org.