Skip to content

Commit 6de934c

Browse files
committed
fix(camera3d): correct Mat4::inverted() transpose bug and Camera3D sync directions
Mat4::inverted() had an extra transpose at the final assignment (inv[col][row] instead of inv[row][col]) producing (A^-1)^T instead of A^-1. Only manifested with non-symmetric matrices like perspective. Also fixes rotation/Euler sync direction in Camera3D: - setRotation() and lookAt(): syncRotationToAngles() (extract angles from new rotation) instead of syncAnglesToRotation() - rotateFPS(): syncAnglesToRotation() (write angles to rotation) instead of syncRotationToAngles()
1 parent c556d52 commit 6de934c

5 files changed

Lines changed: 68 additions & 85 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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ class Mat4 {
158158
Mat4 result;
159159
for (u32 col = 0; col < 4; ++col)
160160
for (u32 row = 0; row < 4; ++row)
161-
result(row, col) = inv[col][row] * invDet;
161+
result(row, col) = inv[row][col] * invDet;
162162
return result;
163163
}
164164

src/render/Camera3D.hpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ class Camera3D {
122122

123123
void setRotation(Quat rot) {
124124
m_rotation = rot;
125-
syncAnglesToRotation();
125+
syncRotationToAngles();
126126
m_dirty = true;
127127
}
128128

@@ -131,7 +131,7 @@ class Camera3D {
131131
Vec3 fwd = (target - eye).normalized();
132132
if (fwd.lengthSquared() < 1e-8f) fwd = {0, 0, 1};
133133
m_rotation = Quat::lookAt(fwd, up);
134-
syncAnglesToRotation();
134+
syncRotationToAngles();
135135
m_dirty = true;
136136
}
137137

@@ -142,7 +142,7 @@ class Camera3D {
142142
m_pitch += deltaPitch;
143143
m_yaw += deltaYaw;
144144
m_pitch = Math::clamp(m_pitch, -89.0f, 89.0f);
145-
syncRotationToAngles();
145+
syncAnglesToRotation();
146146
m_dirty = true;
147147
}
148148

tests/test_camera3d.cpp

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,21 @@ TEST_CASE("Camera3D screenToWorld roundtrip", "[camera3d]") {
131131
Camera3D cam;
132132
cam.lookAt({0, 0, -5}, {0, 0, 0});
133133
cam.setViewport({{0, 0}, {1280, 720}});
134-
Vec3 screen = cam.worldToScreen({0, 0, 0});
135-
REQUIRE(screen.x == Approx(640.0f).margin(10.0f));
136-
REQUIRE(screen.y == Approx(360.0f).margin(10.0f));
134+
Vec3 world = {1.0f, 2.0f, -3.0f};
135+
Vec3 screen = cam.worldToScreen(world);
136+
Vec3 world2 = cam.screenToWorld({screen.x, screen.y}, screen.z);
137+
REQUIRE(world2.x == Approx(world.x).margin(0.01f));
138+
REQUIRE(world2.y == Approx(world.y).margin(0.01f));
139+
REQUIRE(world2.z == Approx(world.z).margin(0.01f));
137140
}
138141

139142
TEST_CASE("Camera3D FPS rotation affects forward", "[camera3d]") {
140143
Camera3D cam;
141-
Vec3 fwd1 = cam.forward();
142-
cam.rotateFPS(0.0f, 45.0f);
143-
Vec3 fwd2 = cam.forward();
144-
REQUIRE(fwd1.length() == Approx(1.0f).margin(0.01f));
145-
REQUIRE(fwd2.length() == Approx(1.0f).margin(0.01f));
144+
cam.rotateFPS(0.0f, 90.0f); // yaw 90 degrees right
145+
Vec3 fwd = cam.forward();
146+
REQUIRE(fwd.x == Approx(1.0f).margin(0.01f));
147+
REQUIRE(fwd.y == Approx(0.0f).margin(0.01f));
148+
REQUIRE(fwd.z == Approx(0.0f).margin(0.01f));
146149
}
147150

148151
TEST_CASE("Camera3D FPS pitch clamped", "[camera3d]") {
@@ -179,8 +182,8 @@ TEST_CASE("Camera3D setRotation", "[camera3d]") {
179182
Quat rot = Quat::fromAxisAngle({0, 1, 0}, degToRad(90.0f));
180183
cam.setRotation(rot);
181184
Vec3 fwd = cam.forward();
182-
REQUIRE(fabsf(fwd.x) < 2.0f);
183-
REQUIRE(fabsf(fwd.z) < 2.0f);
185+
REQUIRE(fwd.x == Approx(1.0f).margin(0.01f));
186+
REQUIRE(fwd.z == Approx(0.0f).margin(0.01f));
184187
}
185188

186189
TEST_CASE("Camera3D setFOV clamping", "[camera3d]") {

0 commit comments

Comments
 (0)