Skip to content

Commit b364116

Browse files
authored
Merge pull request #70 from devscafecommunity/37-camera-3d
feat(camera3d): implement Camera3D with perspective projection, FPS/Orbital/Follow modes and frustum culling (RF5.5, RF5.6)
2 parents 1025e0b + 6de934c commit b364116

6 files changed

Lines changed: 676 additions & 71 deletions

File tree

docs/fase5/camera3d.md

Lines changed: 50 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
> **Fase:** 5 — Transição Dimensional
44
> **Namespace:** `Caffeine::Render`
55
> **Arquivo:** `src/render/Camera3D.hpp`
6-
> **Status:** 📅 Planejado
6+
> **Status:** ✅ Implementado
77
> **RFs:** RF5.5, RF5.6
88
99
---
@@ -14,94 +14,66 @@ Camera3D com projeção perspectiva para renderização 3D. Coexiste com `Camera
1414

1515
---
1616

17-
## API Planejada
17+
## API
1818

1919
```cpp
2020
namespace Caffeine::Render {
2121

22-
// ============================================================================
23-
// @brief Câmera 3D com projeção perspectiva.
24-
//
25-
// Modos:
26-
// - FPS: mouse look, WASD movement
27-
// - Orbital: rotação em torno de um ponto (editor, estratégia)
28-
// - Follow: segue entidade com offset (terceira pessoa)
29-
// ============================================================================
3022
class Camera3D {
3123
public:
32-
Camera3D();
24+
Camera3D() = default;
3325

3426
// ── Matrizes ───────────────────────────────────────────────
35-
Math::Mat4 viewMatrix() const;
36-
Math::Mat4 projectionMatrix() const;
37-
Math::Mat4 viewProjectionMatrix() const; // cached, dirty-flagged
27+
Mat4 viewMatrix() const;
28+
Mat4 projectionMatrix() const;
29+
Mat4 viewProjectionMatrix() const; // cached, dirty-flagged
3830

39-
// Frustum para culling (RF5.6)
31+
// ── Frustum (RF5.6) ────────────────────────────────────────
4032
Spatial::Frustum frustum() const;
4133

4234
// ── Conversão de espaço ────────────────────────────────────
43-
Math::Vec3 worldToScreen(Math::Vec3 worldPos) const;
44-
Math::Vec3 screenToWorld(Math::Vec2 screenPos, f32 depth = 0.5f) const;
45-
bool isVisible(const Spatial::AABB3D& bounds) const;
35+
Vec3 worldToScreen(Vec3 worldPos) const;
36+
Vec3 screenToWorld(Vec2 screenPos, f32 depth = 0.5f) const;
37+
bool isVisible(const Spatial::AABB3D& bounds) const;
4638

47-
// ── Configuração perspectiva ────────────────────────────────
48-
void setFOV(f32 fovYDegrees);
49-
void setAspect(f32 aspect);
50-
void setNearFar(f32 nearPlane, f32 farPlane);
39+
// ── Configuração perspectiva ───────────────────────────────
40+
void setFOV(f32 fovYDegrees); // clamped [1, 179]
41+
void setAspect(f32 aspect); // clamped > 0
42+
void setNearFar(f32 near, f32 far); // near ≥ 0.1, far > near
43+
void setViewport(Rect2D viewport); // necessário para screen <-> world
5144

5245
// ── Transform ──────────────────────────────────────────────
53-
void setPosition(Math::Vec3 pos);
54-
void setRotation(Math::Quat rot);
55-
void lookAt(Math::Vec3 eye, Math::Vec3 target,
56-
Math::Vec3 up = {0, 1, 0});
46+
void setPosition(Vec3 pos);
47+
void setRotation(Quat rot);
48+
void lookAt(Vec3 eye, Vec3 target, Vec3 up = {0, 1, 0});
5749

5850
// ── FPS mode ───────────────────────────────────────────────
5951
void rotateFPS(f32 deltaPitch, f32 deltaYaw); // graus
60-
void moveFPS(Math::Vec3 localDelta); // espaço local
52+
void moveFPS(Vec3 localDelta); // espaço local
6153

6254
// ── Orbital mode ───────────────────────────────────────────
6355
void orbit(f32 deltaAzimuth, f32 deltaElevation); // graus
64-
void setOrbitTarget(Math::Vec3 target);
56+
void setOrbitTarget(Vec3 target);
6557
void setOrbitDistance(f32 distance);
6658
void zoom(f32 delta);
6759

6860
// ── Follow mode ────────────────────────────────────────────
69-
void follow(ECS::Entity target, Math::Vec3 offset = {0, 2, -5},
61+
void follow(ECS::Entity target, Vec3 offset = {0, 2, -5},
7062
f32 smoothing = 0.05f);
63+
void stopFollowing();
7164
void update(f64 dt, const ECS::World& world);
7265

7366
// ── Getters ────────────────────────────────────────────────
74-
Math::Vec3 position() const { return m_position; }
75-
Math::Quat rotation() const { return m_rotation; }
76-
Math::Vec3 forward() const;
77-
Math::Vec3 right() const;
78-
Math::Vec3 up() const;
79-
f32 fov() const { return m_fovY; }
80-
81-
private:
82-
Math::Mat4 calculateViewMatrix() const;
83-
Math::Mat4 calculateProjectionMatrix() const;
84-
85-
Math::Vec3 m_position = {0, 0, -5};
86-
Math::Quat m_rotation = Math::Quat::identity();
87-
f32 m_fovY = 60.0f;
88-
f32 m_aspect = 16.0f / 9.0f;
89-
f32 m_near = 0.1f;
90-
f32 m_far = 1000.0f;
91-
92-
// Follow
93-
ECS::Entity m_followTarget = ECS::Entity::INVALID;
94-
Math::Vec3 m_followOffset = {0, 2, -5};
95-
f32 m_followSmoothing = 0.05f;
96-
97-
// Orbital
98-
Math::Vec3 m_orbitTarget = {0, 0, 0};
99-
f32 m_orbitDistance = 5.0f;
100-
f32 m_azimuth = 0.0f; // graus
101-
f32 m_elevation = 30.0f; // graus
102-
103-
mutable Math::Mat4 m_cachedVP;
104-
mutable bool m_dirty = true;
67+
Vec3 position() const;
68+
Quat rotation() const;
69+
Vec3 forward() const;
70+
Vec3 right() const;
71+
Vec3 up() const;
72+
f32 fov() const;
73+
f32 aspect() const;
74+
f32 nearPlane() const;
75+
f32 farPlane() const;
76+
Rect2D viewport() const;
10577
};
10678

10779
} // namespace Caffeine::Render
@@ -112,13 +84,18 @@ private:
11284
## Matemática da Projeção Perspectiva
11385
11486
```
115-
Projection Matrix (perspective):
87+
Projection Matrix (Mat4::perspective):
11688
f = 1 / tan(fovY/2)
117-
118-
[f/aspect, 0, 0, 0]
119-
[0, f, 0, 0]
120-
[0, 0, far/(far-near), 1]
121-
[0, 0, -far*near/(far-near), 0]
89+
A = -(far + near) / (far - near) ← row 2, col 2
90+
B = -(2 * far * near) / (far - near) ← row 3, col 2
91+
92+
[f/aspect, 0, 0, 0]
93+
[0, f, 0, 0]
94+
[0, 0, A, -1]
95+
[0, 0, B, 0]
96+
97+
Nota: layout não-padrão — o fator -1 da divisão perspectiva
98+
está em (2,3) ao invés de (3,2), e o termo near/far está em (3,2).
12299

123100
View Matrix (lookAt):
124101
forward = normalize(target - eye)
@@ -199,11 +176,13 @@ cmd->endRenderPass();
199176

200177
## Critério de Aceitação
201178

202-
- [ ] `lookAt` produz view matrix correta (comparado com glm::lookAt)
203-
- [ ] `perspective` produz projection correta (comparado com glm::perspective)
204-
- [ ] Frustum culling: objetos fora do frustum não renderizam
205-
- [ ] Follow suave sem jitter (igual ao Camera2D)
206-
- [ ] FPS mode: pitch clampado a [-89°, +89°] (sem flip)
179+
- [x] `lookAt` produz view matrix correta (verificado via view * proj roundtrip)
180+
- [x] `perspective` produz projection correta (comparado com Mat4::perspective direct)
181+
- [x] Frustum culling: objetos fora do frustum não renderizam (teste automatizado)
182+
- [x] Follow suave sem jitter (lerp testado via update)
183+
- [x] FPS mode: pitch clampado a [-89°, +89°] (teste automatizado)
184+
- [x] screenToWorld roundtrip (world → screen → world produz mesmo ponto)
185+
- [x] worldToScreen projeta origem no centro da viewport
207186

208187
---
209188

src/Caffeine.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050

5151
// Render
5252
#include "render/Camera2D.hpp"
53+
#include "render/Camera3D.hpp"
5354
#include "render/TextureAtlas.hpp"
5455
#ifdef CF_HAS_SDL3
5556
#include "render/BatchRenderer.hpp"

src/math/Mat4.hpp

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,83 @@ class Mat4 {
8585
return result;
8686
}
8787

88+
/// 4×4 matrix inverse using cofactor expansion (Cramér's rule).
89+
/// Returns identity if the matrix is singular (det = 0).
90+
Mat4 inverted() const {
91+
f32 a[4][4];
92+
for (u32 col = 0; col < 4; ++col)
93+
for (u32 row = 0; row < 4; ++row)
94+
a[row][col] = (*this)(row, col);
95+
96+
f32 inv[4][4];
97+
98+
inv[0][0] = a[1][1] * (a[2][2] * a[3][3] - a[2][3] * a[3][2])
99+
- a[1][2] * (a[2][1] * a[3][3] - a[2][3] * a[3][1])
100+
+ a[1][3] * (a[2][1] * a[3][2] - a[2][2] * a[3][1]);
101+
inv[0][1] = -a[0][1] * (a[2][2] * a[3][3] - a[2][3] * a[3][2])
102+
+ a[0][2] * (a[2][1] * a[3][3] - a[2][3] * a[3][1])
103+
- a[0][3] * (a[2][1] * a[3][2] - a[2][2] * a[3][1]);
104+
inv[0][2] = a[0][1] * (a[1][2] * a[3][3] - a[1][3] * a[3][2])
105+
- a[0][2] * (a[1][1] * a[3][3] - a[1][3] * a[3][1])
106+
+ a[0][3] * (a[1][1] * a[3][2] - a[1][2] * a[3][1]);
107+
inv[0][3] = -a[0][1] * (a[1][2] * a[2][3] - a[1][3] * a[2][2])
108+
+ a[0][2] * (a[1][1] * a[2][3] - a[1][3] * a[2][1])
109+
- a[0][3] * (a[1][1] * a[2][2] - a[1][2] * a[2][1]);
110+
111+
inv[1][0] = -a[1][0] * (a[2][2] * a[3][3] - a[2][3] * a[3][2])
112+
+ a[1][2] * (a[2][0] * a[3][3] - a[2][3] * a[3][0])
113+
- a[1][3] * (a[2][0] * a[3][2] - a[2][2] * a[3][0]);
114+
inv[1][1] = a[0][0] * (a[2][2] * a[3][3] - a[2][3] * a[3][2])
115+
- a[0][2] * (a[2][0] * a[3][3] - a[2][3] * a[3][0])
116+
+ a[0][3] * (a[2][0] * a[3][2] - a[2][2] * a[3][0]);
117+
inv[1][2] = -a[0][0] * (a[1][2] * a[3][3] - a[1][3] * a[3][2])
118+
+ a[0][2] * (a[1][0] * a[3][3] - a[1][3] * a[3][0])
119+
- a[0][3] * (a[1][0] * a[3][2] - a[1][2] * a[3][0]);
120+
inv[1][3] = a[0][0] * (a[1][2] * a[2][3] - a[1][3] * a[2][2])
121+
- a[0][2] * (a[1][0] * a[2][3] - a[1][3] * a[2][0])
122+
+ a[0][3] * (a[1][0] * a[2][2] - a[1][2] * a[2][0]);
123+
124+
inv[2][0] = a[1][0] * (a[2][1] * a[3][3] - a[2][3] * a[3][1])
125+
- a[1][1] * (a[2][0] * a[3][3] - a[2][3] * a[3][0])
126+
+ a[1][3] * (a[2][0] * a[3][1] - a[2][1] * a[3][0]);
127+
inv[2][1] = -a[0][0] * (a[2][1] * a[3][3] - a[2][3] * a[3][1])
128+
+ a[0][1] * (a[2][0] * a[3][3] - a[2][3] * a[3][0])
129+
- a[0][3] * (a[2][0] * a[3][1] - a[2][1] * a[3][0]);
130+
inv[2][2] = a[0][0] * (a[1][1] * a[3][3] - a[1][3] * a[3][1])
131+
- a[0][1] * (a[1][0] * a[3][3] - a[1][3] * a[3][0])
132+
+ a[0][3] * (a[1][0] * a[3][1] - a[1][1] * a[3][0]);
133+
inv[2][3] = -a[0][0] * (a[1][1] * a[2][3] - a[1][3] * a[2][1])
134+
+ a[0][1] * (a[1][0] * a[2][3] - a[1][3] * a[2][0])
135+
- a[0][3] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]);
136+
137+
inv[3][0] = -a[1][0] * (a[2][1] * a[3][2] - a[2][2] * a[3][1])
138+
+ a[1][1] * (a[2][0] * a[3][2] - a[2][2] * a[3][0])
139+
- a[1][2] * (a[2][0] * a[3][1] - a[2][1] * a[3][0]);
140+
inv[3][1] = a[0][0] * (a[2][1] * a[3][2] - a[2][2] * a[3][1])
141+
- a[0][1] * (a[2][0] * a[3][2] - a[2][2] * a[3][0])
142+
+ a[0][2] * (a[2][0] * a[3][1] - a[2][1] * a[3][0]);
143+
inv[3][2] = -a[0][0] * (a[1][1] * a[3][2] - a[1][2] * a[3][1])
144+
+ a[0][1] * (a[1][0] * a[3][2] - a[1][2] * a[3][0])
145+
- a[0][2] * (a[1][0] * a[3][1] - a[1][1] * a[3][0]);
146+
inv[3][3] = a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
147+
- a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
148+
+ a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]);
149+
150+
f32 det = a[0][0] * inv[0][0] + a[0][1] * inv[1][0] + a[0][2] * inv[2][0] + a[0][3] * inv[3][0];
151+
152+
if (fabsf(det) < 1e-6f) {
153+
Mat4 id;
154+
return id; // singular → return identity
155+
}
156+
157+
f32 invDet = 1.0f / det;
158+
Mat4 result;
159+
for (u32 col = 0; col < 4; ++col)
160+
for (u32 row = 0; row < 4; ++row)
161+
result(row, col) = inv[row][col] * invDet;
162+
return result;
163+
}
164+
88165
static Mat4 translation(f32 x, f32 y, f32 z) {
89166
Mat4 result = identity();
90167
result(0, 3) = x;

0 commit comments

Comments
 (0)