AABB — Axis-Aligned Bounding Box
Une AABB est une boîte alignée sur les axes (X, Y, Z). Elle est utilisée partout en 3D pour des tests de collision ultra-rapides — c'est l'une des structures les plus importantes d'un moteur de jeu.
↗ Démo interactive Three.js — gravité + collisions AABB
À ne pas négliger
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* + + */
/* File: AABB.cpp +++ +++ */
/* + + */
/* By: Laboitederemdal + + */
/* + + */
/* Created: 27/10/2025 18:44:15 + + + + + + */
/* */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef AABB_hpp
#define AABB_hpp
struct AABB
{
simd::float3 minimum;
simd::float3 maximum;
simd::float3 center() const { return (minimum + maximum) * 0.5f; }
simd::float3 extents() const { return (maximum - minimum) * 0.5f; }
bool intersects(const AABB& o) const
{
return (minimum.x <= o.maximum.x && maximum.x >= o.minimum.x)
&& (minimum.y <= o.maximum.y && maximum.y >= o.minimum.y)
&& (minimum.z <= o.maximum.z && maximum.z >= o.minimum.z);
}
static AABB fromVertices(const std::vector<simd::float3>& verts)
{
AABB b{ verts[0], verts[0] };
for (auto& v : verts)
{
b.minimum = simd::min(b.minimum, v);
b.maximum = simd::max(b.maximum, v);
}
return b;
}
};
AABB transformAABB(const AABB& local, const simd::float4x4& M)
{
simd::float3 newMin = simd_make_float3(M.columns[3].x, M.columns[3].y, M.columns[3].z);
simd::float3 newMax = newMin;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
float e = M.columns[j][i] * local.minimum[j];
float f = M.columns[j][i] * local.maximum[j];
if (e < f)
{
newMin[i] += e;
newMax[i] += f;
}
else
{
newMin[i] += f;
newMax[i] += e;
}
}
}
return { newMin, newMax };
}
struct BoxCollider
{
AABB localAABB;
AABB worldAABB;
bool isTrigger = false;
bool isStatic = false;
uint32_t layerMask = 0xFFFFFFFF;
};
#endif /* AABB_hpp */
Schéma mental — ce qu'est une AABB
maximum (coin haut-droit-avant)
+---------+
/ | /|
/ | / |
+---------+ |
| | | |
| +-----|---+
| / | /
| / | /
+---------+
minimum (coin bas-gauche-arrière)
Une AABB est définie uniquement par deux points : minimum (coin le plus petit sur tous les axes) et maximum (coin le plus grand). Tout le reste — centre, taille, test de collision — se calcule depuis ces deux valeurs.
center() — milieu de la boîte
simd::float3 center() const { return (minimum + maximum) * 0.5f; }
// Exemple :
// minimum = {1, 0, 1} maximum = {3, 2, 3}
// center = {2, 1, 2} ← milieu exact sur chaque axe
Utilisé pour le frustum culling (tester si le centre d'un objet est visible), les LOD, et les effets de particules centrés sur un objet.
extents() — demi-tailles
simd::float3 extents() const { return (maximum - minimum) * 0.5f; }
// minimum = {0, 0, 0} maximum = {4, 2, 6}
// extents = {2, 1, 3} ← rayon de la boîte sur chaque axe
Les extents sont les "rayons" de la boîte. Très utilisés pour les tests de collision avec des sphères (length(point - center) < length(extents)) et pour le frustum culling côté GPU.
intersects() — test de collision AABB vs AABB
bool intersects(const AABB& o) const
{
return (minimum.x <= o.maximum.x && maximum.x >= o.minimum.x) // axe X
&& (minimum.y <= o.maximum.y && maximum.y >= o.minimum.y) // axe Y
&& (minimum.z <= o.maximum.z && maximum.z >= o.minimum.z); // axe Z
}
Principe du théorème de l'axe séparateur simplifié : deux AABB ne se touchent pas s'il existe au moins un axe sur lequel elles sont séparées. On teste les 3 axes — si toutes les conditions sont vraies, il y a collision.
// Visualisation 2D (vue de dessus) :
//
// [A]────── ──────[B] séparées sur X → pas de collision
//
// [A]─────[B] chevauchement sur X ET Y → collision !
// ───
fromVertices() — construire depuis un mesh
static AABB fromVertices(const std::vector<simd::float3>& verts)
{
AABB b{ verts[0], verts[0] }; // init avec le premier vertex
for (auto& v : verts)
{
b.minimum = simd::min(b.minimum, v); // garde le plus petit
b.maximum = simd::max(b.maximum, v); // garde le plus grand
}
return b;
}
// Usage depuis assimpBlender :
std::vector<simd::float3> positions;
for (auto& v : model.vertices)
positions.push_back(v.position);
AABB localBox = AABB::fromVertices(positions);
transformAABB() — local → monde
Problème classique : une AABB est définie dans l'espace local de l'objet (aligned). Après rotation de l'objet, elle n'est plus alignée sur les axes mondiaux. On ne peut pas la tourner — on reconstruit une nouvelle AABB mondiale qui englobe le résultat transformé.
// Sans transformAABB — FAUX après rotation :
//
// Local AABB Après rotation 45° AABB recalculée (correcte)
// ┌──────┐ ╱╲ ┌────────┐
// │ │ ╱ ╲ │ ╱╲ │
// │ │ ╱ ╲ │ ╱ ╲ │
// └──────┘ ╲ ╱ │ ╲ ╱ │
// ╲ ╱ │ ╲╱ │
// ╲╱ └────────┘
AABB transformAABB(const AABB& local, const simd::float4x4& M)
{
// Départ : position de la translation (colonne 3 de la matrice)
simd::float3 newMin = { M.columns[3].x, M.columns[3].y, M.columns[3].z };
simd::float3 newMax = newMin;
// Pour chaque axe de sortie i (X, Y, Z) :
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
// Projeter les deux extrêmes de la boîte locale sur l'axe transformé
float e = M.columns[j][i] * local.minimum[j];
float f = M.columns[j][i] * local.maximum[j];
// Accumuler min et max
if (e < f)
{
newMin[i] += e;
newMax[i] += f;
}
else
{
newMin[i] += f;
newMax[i] += e;
}
}
}
return { newMin, newMax };
}
// Usage — à appeler quand l'objet bouge :
collider.worldAABB = transformAABB(collider.localAABB, model.transform);
BoxCollider — le composant complet
struct BoxCollider
{
AABB localAABB; // espace local — ne change pas
AABB worldAABB; // espace monde — mis à jour chaque frame si mobile
bool isTrigger = false; // détecte sans réponse physique
bool isStatic = false; // ne bouge pas → pas de recalcul worldAABB
uint32_t layerMask = 0xFFFFFFFF; // filtre par couche (0x01 = sol, 0x02 = ennemis…)
};
// Boucle de collision simple :
for (auto& a : colliders)
{
for (auto& b : colliders)
{
if (&a == &b) continue;
if (!(a.layerMask & b.layerMask))
continue; // filtre couches
if (a.worldAABB.intersects(b.worldAABB))
resolveCollision(a, b);
}
}
AABBRenderer — un rendu pour voir ses boîtes
Une AABB qu'on ne voit pas est une AABB qu'on ne débugge pas. Tant que intersects() n'est qu'un bool dans la console, impossible de savoir si la boîte du joueur est deux fois trop grande ou décalée de 50 unités sur Y. On ajoute donc une classe de rendu — AABBRenderer — au même format que Perspective ou Game2D : constructeur (device, library, pixel formats), buildGeometry(), buildPipeline(), draw().
center + corner * extents. Résultat : toutes les boîtes de la scène en un seul draw call, en MTL::PrimitiveTypeLine.
Les fichiers à ajouter
MSLearn/
├── Renderer/
│ ├── Renderer.cpp ← intégration
│ ├── Renderer.hpp ← intégration
│ ├── AABB.hpp ← 2 ajouts (inline + hit + layers)
│ ├── AABBRenderer.cpp ← nouveau
│ └── AABBRenderer.hpp ← nouveau
├── Shaders/
│ └── AABBRenderer.metal ← nouveau
└── includes/
└── SharedGPU/
└── Mesh_shared.h ← BoxInstance
1 — BoxInstance : la donnée partagée CPU ↔ GPU
/// C++ — includes/SharedGPU/Mesh_shared.h — à côté de CubeInstance
struct alignas(16) BoxInstance
{
simd::float4 center; // xyz = centre monde, w inutilisé
simd::float4 extents; // xyz = demi-tailles, w inutilisé
simd::float4 color; // rgba — a < 1.0 pour un trigger translucide
};
48 octets, alignés sur 16 : le GPU lit ce tableau tel quel, sans padding surprise. On stocke centre + extents plutôt que minimum + maximum parce que c'est exactement ce dont le shader a besoin — center() et extents() de l'annexe sont faits pour ça.
2 — Deux ajouts dans AABB.hpp
/// C++ — AABB.hpp — update:11/09/26
enum : uint32_t // une couche par bit — 32 couches maximum
{
kLayerDefault = 0x01,
kLayerPlayer = 0x02,
kLayerTerrain = 0x04,
kLayerTrigger = 0x08,
};
struct BoxCollider
{
AABB localAABB;
AABB worldAABB;
bool isTrigger = false;
bool isStatic = false;
bool hit = false; // ← ajout : résultat de la frame, sert à colorer
uint32_t layerMask = 0xFFFFFFFF;
};
// ← ajout : inline. Le .hpp est maintenant inclus par Renderer.cpp ET AABBRenderer.cpp,
// sans inline le linker hurle "duplicate symbol _transformAABB"
inline AABB transformAABB(const AABB& local, const simd::float4x4& M)
3 — AABBRenderer.hpp
/// C++ — AABBRenderer.hpp — update:11/09/26
#ifndef AABBRenderer_hpp
#define AABBRenderer_hpp
#include "AABB.hpp"
#include "../includes/SharedGPU/Mesh_shared.h"
#include "../includes/SharedGPU/Renderer_shared.h"
class AABBRenderer
{
public:
AABBRenderer(MTL::Device* device, MTL::Library* shaderLibrary,
MTL::PixelFormat colorPixelFormat, MTL::PixelFormat depthPixelFormat);
~AABBRenderer();
void beginFrame(); // remet le compteur d'instances à 0
void submit(const AABB& box, simd::float4 color); // 1 boîte = 1 instance
void submit(const BoxCollider& collider); // couleur automatique
void draw(MTL::RenderCommandEncoder* renderCommandEncoder,
const GlobalUniforms& globalUniforms);
size_t count() const { return m_count; }
private:
void buildGeometry(MTL::Device* device);
void buildPipeline(MTL::Device* device, MTL::Library* shaderLibrary,
MTL::PixelFormat colorPixelFormat, MTL::PixelFormat depthPixelFormat);
static constexpr size_t kMaxBoxes = 1024; // 1024 × 48 o = 48 Ko de buffer
NS::SharedPtr<MTL::Buffer> m_cornerBuffer; // 8 sommets ±1
NS::SharedPtr<MTL::Buffer> m_indexBuffer; // 24 indices = 12 arêtes
NS::SharedPtr<MTL::Buffer> m_instanceBuffer; // kMaxBoxes × BoxInstance
NS::SharedPtr<MTL::RenderPipelineState> m_renderPipelineState;
NS::SharedPtr<MTL::DepthStencilState> m_depthStencilState;
size_t m_count = 0;
};
#endif /* AABBRenderer_hpp */
4 — buildGeometry() : 8 sommets, 12 arêtes
Les sommets sont en ±1, pas en ±0.5 : le shader multiplie directement par extents() (qui est déjà une demi-taille), aucun facteur 2 à traîner.
// 7 ________ 6 z
// /| /| ↑ y
// 3 /_|____ / | 2 | ↗
// | |4___|__| 5 |/___→ x
// | / | /
// |/______|/ 0 = (-1,-1,-1) → minimum
// 0 1 6 = (+1,+1,+1) → maximum
/// C++ — AABBRenderer.cpp — update:11/09/26
#include "AABBRenderer.hpp"
AABBRenderer::AABBRenderer(MTL::Device* device, MTL::Library* shaderLibrary,
MTL::PixelFormat colorPixelFormat, MTL::PixelFormat depthPixelFormat)
{
buildGeometry(device);
buildPipeline(device, shaderLibrary, colorPixelFormat, depthPixelFormat);
}
AABBRenderer::~AABBRenderer() {} // NS::SharedPtr relâche tout seul
void AABBRenderer::buildGeometry(MTL::Device* device)
{ // Boîte unitaire : le shader fera center + corner * extents
const simd::float3 corners[8] = {
{ -1, -1, -1 }, // 0 ← minimum
{ 1, -1, -1 }, // 1
{ 1, 1, -1 }, // 2
{ -1, 1, -1 }, // 3
{ -1, -1, 1 }, // 4
{ 1, -1, 1 }, // 5
{ 1, 1, 1 }, // 6 ← maximum
{ -1, 1, 1 }, // 7
};
const uint16_t edges[24] = { // 12 arêtes × 2 sommets — index list en lignes
0,1, 1,2, 2,3, 3,0, // face arrière z = -1
4,5, 5,6, 6,7, 7,4, // face avant z = +1
0,4, 1,5, 2,6, 3,7, // les 4 montants
};
m_cornerBuffer = NS::TransferPtr(device->newBuffer(corners, sizeof(corners), MTL::ResourceStorageModeShared));
m_indexBuffer = NS::TransferPtr(device->newBuffer(edges, sizeof(edges), MTL::ResourceStorageModeShared));
m_instanceBuffer = NS::TransferPtr(device->newBuffer(sizeof(BoxInstance) * kMaxBoxes,
MTL::ResourceStorageModeShared));
}
constant float3* corners [[buffer(0)]] du shader lit donc le tableau avec le bon stride, sans descripteur de vertex — même stratégie que vertex_main_perspective.
5 — buildPipeline() : lignes, et surtout pas d'écriture de profondeur
/// C++ — AABBRenderer.cpp — update:11/09/26
void AABBRenderer::buildPipeline(MTL::Device* device, MTL::Library* shaderLibrary,
MTL::PixelFormat colorPixelFormat, MTL::PixelFormat depthPixelFormat)
{
auto vertexFunction = NS::TransferPtr(shaderLibrary->newFunction(MTLSTR("vertex_main_aabb")));
auto fragmentFunction = NS::TransferPtr(shaderLibrary->newFunction(MTLSTR("fragment_main_aabb")));
auto renderPipelineDescriptor = NS::TransferPtr(MTL::RenderPipelineDescriptor::alloc()->init());
renderPipelineDescriptor->setVertexFunction(vertexFunction.get());
renderPipelineDescriptor->setFragmentFunction(fragmentFunction.get());
renderPipelineDescriptor->setDepthAttachmentPixelFormat(depthPixelFormat);
auto colorAttachment = renderPipelineDescriptor->colorAttachments()->object(0);
colorAttachment->setPixelFormat(colorPixelFormat);
colorAttachment->setBlendingEnabled(true); // alpha < 1 pour les triggers
colorAttachment->setRgbBlendOperation(MTL::BlendOperationAdd);
colorAttachment->setAlphaBlendOperation(MTL::BlendOperationAdd);
colorAttachment->setSourceRGBBlendFactor(MTL::BlendFactorSourceAlpha);
colorAttachment->setSourceAlphaBlendFactor(MTL::BlendFactorSourceAlpha);
colorAttachment->setDestinationRGBBlendFactor(MTL::BlendFactorOneMinusSourceAlpha);
colorAttachment->setDestinationAlphaBlendFactor(MTL::BlendFactorOneMinusSourceAlpha);
NS::Error* error = nullptr;
m_renderPipelineState = NS::TransferPtr(device->newRenderPipelineState(renderPipelineDescriptor.get(), &error));
assert(!error && "AABB pipeline creation failed");
auto _depthStencilDescriptor = NS::TransferPtr(MTL::DepthStencilDescriptor::alloc()->init());
_depthStencilDescriptor->setDepthCompareFunction(MTL::CompareFunction::CompareFunctionLessEqual);
_depthStencilDescriptor->setDepthWriteEnabled(false); // ← le plus important de ce fichier
m_depthStencilState = NS::TransferPtr(device->newDepthStencilState(_depthStencilDescriptor.get()));
}
6 — beginFrame() / submit() : remplir le buffer d'instances
/// C++ — AABBRenderer.cpp — update:11/09/26
void AABBRenderer::beginFrame()
{
m_count = 0; // on réécrit par-dessus, rien à effacer
}
void AABBRenderer::submit(const AABB& box, simd::float4 color)
{
if (m_count >= kMaxBoxes)
return; // perdre une boîte de debug > écrire hors du buffer
BoxInstance* instances = static_cast<BoxInstance*>(m_instanceBuffer->contents());
simd::float3 c = box.center();
simd::float3 e = box.extents();
instances[m_count].center = { c.x, c.y, c.z, 1.f };
instances[m_count].extents = { e.x, e.y, e.z, 0.f };
instances[m_count].color = color;
m_count++;
}
void AABBRenderer::submit(const BoxCollider& collider)
{ // la couleur EST l'information de debug
constexpr simd::float4 kGreen = { 0.15f, 0.95f, 0.35f, 1.0f }; // libre
constexpr simd::float4 kRed = { 1.0f, 0.18f, 0.18f, 1.0f }; // en collision
constexpr simd::float4 kYellow = { 1.0f, 0.92f, 0.10f, 0.55f }; // trigger
constexpr simd::float4 kGrey = { 0.55f, 0.58f, 0.65f, 1.0f }; // statique
simd::float4 color = kGreen;
if (collider.hit) color = kRed;
else if (collider.isTrigger) color = kYellow;
else if (collider.isStatic) color = kGrey;
submit(collider.worldAABB, color);
}
7 — draw() : un seul draw call pour toute la scène
/// C++ — AABBRenderer.cpp — update:11/09/26
void AABBRenderer::draw(MTL::RenderCommandEncoder* renderCommandEncoder,
const GlobalUniforms& globalUniforms)
{
if (m_count == 0)
return; // rien soumis cette frame → on ne touche même pas au pipeline
renderCommandEncoder->setRenderPipelineState(m_renderPipelineState.get());
renderCommandEncoder->setDepthStencilState(m_depthStencilState.get());
renderCommandEncoder->setVertexBuffer(m_cornerBuffer.get(), 0, 0);
renderCommandEncoder->setVertexBytes(&globalUniforms, sizeof(GlobalUniforms), 1);
renderCommandEncoder->setVertexBuffer(m_instanceBuffer.get(), 0, 2); // pas setVertexBytes : 48 Ko > 4 Ko
renderCommandEncoder->drawIndexedPrimitives(MTL::PrimitiveTypeLine,
NS::UInteger(24), // 12 arêtes × 2
MTL::IndexTypeUInt16,
m_indexBuffer.get(), 0,
NS::UInteger(m_count)); // instanceCount
}
8 — AABBRenderer.metal
/// MSL — Shaders/AABBRenderer.metal — update:11/09/26
#include <metal_stdlib>
#include "../includes/SharedGPU/Mesh_shared.h"
#include "../includes/SharedGPU/Renderer_shared.h"
using namespace metal;
struct v2fBox
{
float4 position [[position]];
half4 color;
};
v2fBox vertex vertex_main_aabb(constant float3* corners [[buffer(0)]],
constant GlobalUniforms& uniforms [[buffer(1)]],
constant BoxInstance* boxes [[buffer(2)]],
uint vertexId [[vertex_id]],
uint iid [[instance_id]])
{
BoxInstance box = boxes[iid];
// corners est en ±1 → on retombe exactement sur minimum / maximum
float3 worldPosition = box.center.xyz + corners[vertexId] * box.extents.xyz;
v2fBox out;
out.position = uniforms.cameraUniforms.viewProjectionMatrix * float4(worldPosition, 1.0);
out.color = half4(box.color);
return out;
}
half4 fragment fragment_main_aabb(v2fBox in [[stage_in]])
{
return in.color; // couleur plate : une ligne de debug ne s'éclaire pas
}
Tout le travail tient en une ligne : center + corner * extents. 8 sommets lus, 24 indices, et la matrice viewProjectionMatrix déjà calculée par Camera — aucun uniform supplémentaire, aucune matrice modèle. C'est le gros avantage d'une boîte alignée sur les axes : elle n'a pas d'orientation à transporter.
9 — Intégration dans Renderer
/// C++ — Renderer.hpp — update:11/09/26
#include "AABBRenderer.hpp"
private:
SpriteRenderer m_sprites;
AABBRenderer m_aabb; // ← déclare-le APRÈS m_sprites…
std::vector<BoxCollider> m_colliders;
size_t m_playerCollider = 0;
size_t m_cameraCollider = 0;
bool m_showColliders = true;
bool m_togglePressed = false; // front montant de la touche
void buildColliders();
void updateColliders();
/// C++ — Renderer.cpp — update:11/09/26 — liste d'initialisation
m_sprites(m_device, m_shaderLibrary, m_pixelFormat, m_depthPixelFormat),
m_aabb(m_device, m_shaderLibrary, m_pixelFormat, m_depthPixelFormat), // …et initialise-le au même rang
frame(0)
.hpp, pas dans l'ordre de la liste d'initialisation. Si les deux divergent, clang sort -Wreorder — garde la même position dans les deux fichiers.
/// C++ — Renderer.cpp — update:11/09/26 — à la fin du constructeur
m_assimp.printAnimations(m_odonata);
m_assimp.printAnimations(m_scene);
buildColliders(); // ← après les loadModel() : on a besoin des vertices
/// C++ — Renderer.cpp — update:11/09/26
void Renderer::buildColliders()
{
m_colliders.clear();
// 1 — le joueur : AABB locale calculée UNE fois depuis le mesh chargé
Blender* odonata = m_assimp.getModel(m_odonata);
std::vector<simd::float3> positions;
positions.reserve(odonata->vertices.size());
for (const VertexBlender& v : odonata->vertices)
positions.push_back(v.position);
BoxCollider player;
player.localAABB = AABB::fromVertices(positions);
player.worldAABB = transformAABB(player.localAABB, odonata->transform);
player.layerMask = kLayerPlayer | kLayerDefault;
m_playerCollider = m_colliders.size();
m_colliders.push_back(player);
// 2 — la caméra : une boîte « corps » de 1 × 1.8 × 1 autour de l'œil
BoxCollider camera;
camera.localAABB = { { -0.5f, -1.6f, -0.5f }, { 0.5f, 0.2f, 0.5f } };
camera.worldAABB = camera.localAABB;
camera.layerMask = kLayerPlayer | kLayerDefault | kLayerTrigger;
m_cameraCollider = m_colliders.size();
m_colliders.push_back(camera);
// 3 — des obstacles statiques : aucun mesh, juste des boîtes posées à la main
const simd::float3 obstacles[] = {
{ 0.f, 6.f, 12.f }, { 10.f, 6.f, -8.f },
{ -14.f, 6.f, 4.f }, { 6.f, 6.f, 22.f },
};
for (const simd::float3& p : obstacles)
{
BoxCollider box;
box.localAABB = { { -3.f, -3.f, -3.f }, { 3.f, 3.f, 3.f } };
box.worldAABB = transformAABB(box.localAABB, math::makeTranslate(p));
box.isStatic = true; // jamais recalculée → gratuit
box.layerMask = kLayerDefault;
m_colliders.push_back(box);
}
// 4 — un trigger : il détecte, il ne repousse pas
BoxCollider trigger;
trigger.localAABB = { { -5.f, -2.f, -5.f }, { 5.f, 2.f, 5.f } };
trigger.worldAABB = transformAABB(trigger.localAABB, math::makeTranslate({ 0.f, 4.f, -18.f }));
trigger.isStatic = true;
trigger.isTrigger = true;
trigger.layerMask = kLayerTrigger;
m_colliders.push_back(trigger);
printf("BoxColliders : %zu\n", m_colliders.size());
}
/// C++ — Renderer.cpp — update:11/09/26
void Renderer::updateColliders()
{
// 1 — ce qui bouge : local → monde (les isStatic ne sont JAMAIS retouchés)
Blender* odonata = m_assimp.getModel(m_odonata);
BoxCollider& player = m_colliders[m_playerCollider];
player.worldAABB = transformAABB(player.localAABB, odonata->transform);
BoxCollider& camera = m_colliders[m_cameraCollider];
camera.worldAABB = transformAABB(camera.localAABB, math::makeTranslate(m_camera.getPosition()));
// 2 — broad phase : n(n-1)/2 paires × 6 comparaisons
for (BoxCollider& c : m_colliders)
c.hit = false;
for (size_t i = 0; i < m_colliders.size(); i++)
{
for (size_t j = i + 1; j < m_colliders.size(); j++)
{
BoxCollider& a = m_colliders[i];
BoxCollider& b = m_colliders[j];
if (a.isStatic && b.isStatic)
continue; // deux murs ne se rentrent jamais dedans
if (!(a.layerMask & b.layerMask))
continue; // filtre de couches
if (!a.worldAABB.intersects(b.worldAABB))
continue;
a.hit = true;
b.hit = true; // ici, plus tard : resolveCollision(a, b)
}
}
// 3 — soumettre au rendu : une instance par collider
m_aabb.beginFrame();
for (const BoxCollider& c : m_colliders)
m_aabb.submit(c);
}
/// C++ — Renderer.cpp — update:11/09/26 — update(), à la fin
m_sprites.setCamera({ 0.f, 0.f }, 1.f);
// toggle du debug draw sur front montant — sinon il clignote 60 fois par seconde
if (m_input.keyLeft && !m_togglePressed)
m_showColliders = !m_showColliders;
m_togglePressed = m_input.keyLeft;
updateColliders(); // après m_camera.updateUniforms() : la caméra a sa position finale
};
/// C++ — Renderer.cpp — update:11/09/26 — draw(), dans la passe principale
m_assimp.draw(renderCommandEncoder, m_globalUniforms);
if (m_showColliders)
m_aabb.draw(renderCommandEncoder, m_globalUniforms); // après les opaques, avant les sprites
m_sprites.drawFrame(m_explosion, m_explosionAnim.frame, /* … */);
m_sprites.flush(renderCommandEncoder);
renderCommandEncoder->endEncoding();
math::makeTranslate({0, 50, 0}) + math::makeScale({3, 3, 3}) additionne les deux matrices terme à terme : la diagonale devient 4 au lieu de 3, et le w de la colonne 3 vaut 2 au lieu de 1. Composer deux transformations c'est makeTranslate(…) * makeScale(…) — et l'ordre compte (scale d'abord, puis translation). Avec +, transformAABB() renverra une boîte fausse, puisqu'il lit la colonne 3 comme une translation et les colonnes 0-2 comme la rotation/échelle.
Ce que tu vois
| Couleur | État | Origine |
|---|---|---|
| Vert | collider mobile libre | hit == false |
| Rouge | au moins une paire en intersection | hit == true |
| Jaune translucide | volume de trigger | isTrigger |
| Gris | obstacle statique | isStatic |
Déplace-toi avec Z/Q/S/D, A/E pour monter-descendre : dès que la boîte de la caméra touche un obstacle, les deux passent au rouge. Flèche gauche coupe et remet l'affichage. Si une boîte est énorme ou au mauvais endroit, c'est ton fromVertices() ou ta matrice modèle qui parle — pas le rendu.
Les pièges de ce rendu
- setVertexBytes est limité à 4 Ko : 1024 instances × 48 o = 48 Ko, donc buffer obligatoire — setVertexBytes ne sert plus qu'aux GlobalUniforms.
- Un seul instanceBuffer suffit ici parce que Renderer::draw() termine par waitUntilCompleted(). Le jour où tu enlèves cette attente, il faut 2 ou 3 buffers tournants, sinon le CPU réécrit les instances que le GPU lit encore.
- L'épaisseur des lignes n'est pas réglable en Metal : toujours 1 pixel. Pour des arêtes épaisses, il faut passer les arêtes en quads orientés écran — un autre shader, une autre histoire.
- La broad phase est en O(n²) : 8 colliders = 28 tests, parfait. 1000 colliders = 499 500 tests par frame. Au-delà de quelques centaines de boîtes, il faut une grille uniforme ou un BVH — les AABB restent la brique de base, c'est juste le parcours qui change.
Résumé
AABB = boîte définie par minimum + maximum
center() → milieu entre les deux coins
extents() → "rayon" sur chaque axe (taille / 2)
intersects() → 6 comparaisons, O(1), test le plus rapide
fromVertices() → encapsule un mesh complet
transformAABB()→ reconstruit la boîte après déplacement/rotation
BoxCollider → wrapping complet : local + world + layers
AABBRenderer → 1 boîte unitaire instanciée, 1 draw call, MTL::PrimitiveTypeLine
beginFrame() → remet le compteur d'instances à 0
submit() → centre + extents + couleur dans le buffer d'instances
draw() → drawIndexedPrimitives(Line, 24, UInt16, indices, 0, count)
depthWrite → false : un calque de debug ne creuse pas le Z-buffer
👉 Si tu comprends ça tu comprends déjà 70% des systèmes de collision d'un moteur 3D, le célèbre jeu Minecraft utilise ce système !