From 54702a5afe9284f5484b0bef3db4721f15d43780 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 22 May 2026 09:52:14 +0200 Subject: [PATCH] =?UTF-8?q?feat(ship-3d):=20look-at=20din=C3=A0mic,=20naus?= =?UTF-8?q?=20alineades=20amb=20el=20path=20(punta+cul)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/core/graphics/wireframe3d.cpp | 25 ++++++++----- source/game/title/ship_animator3d.cpp | 53 +++++++++++++++++++++++---- source/game/title/ship_animator3d.hpp | 4 ++ 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/source/core/graphics/wireframe3d.cpp b/source/core/graphics/wireframe3d.cpp index e768b81..4408feb 100644 --- a/source/core/graphics/wireframe3d.cpp +++ b/source/core/graphics/wireframe3d.cpp @@ -18,17 +18,12 @@ namespace Graphics { .z = local.z * transform.scale, }; - // 2. Rotació Y (yaw): X i Z. - const float CY = std::cos(transform.rotation_euler.y); - const float SY = std::sin(transform.rotation_euler.y); - { - const float NX = (v.x * CY) + (v.z * SY); - const float NZ = (-v.x * SY) + (v.z * CY); - v.x = NX; - v.z = NZ; - } + // Ordre X → Y → Z: amb aquest ordre, una rotació pitch+yaw pot dur el + // vector local (0,-1,0) a qualsevol direcció mundial — necessari perquè + // les naus calculen pitch+yaw look-at per alinear-se amb el seu path. + // L'ordre invers (Y→X) no permet X arbitrari en vectors sobre l'eix Y. - // 3. Rotació X (pitch): Y i Z. + // 2. Rotació X (pitch): Y i Z. const float CX = std::cos(transform.rotation_euler.x); const float SX = std::sin(transform.rotation_euler.x); { @@ -38,6 +33,16 @@ namespace Graphics { v.z = NZ; } + // 3. Rotació Y (yaw): X i Z. + const float CY = std::cos(transform.rotation_euler.y); + const float SY = std::sin(transform.rotation_euler.y); + { + const float NX = (v.x * CY) + (v.z * SY); + const float NZ = (-v.x * SY) + (v.z * CY); + v.x = NX; + v.z = NZ; + } + // 4. Rotació Z (roll): X i Y. const float CZ = std::cos(transform.rotation_euler.z); const float SZ = std::sin(transform.rotation_euler.z); diff --git a/source/game/title/ship_animator3d.cpp b/source/game/title/ship_animator3d.cpp index 42e87e4..d28ecca 100644 --- a/source/game/title/ship_animator3d.cpp +++ b/source/game/title/ship_animator3d.cpp @@ -18,12 +18,6 @@ namespace Title { // 0.0F → emet només la silueta plana. >0 emet volum extrudit. constexpr float SHIP_EXTRUSION_DEPTH = 1.0F; - // Rotació pitch que aplica el draw() per orientar la silueta cap al punt - // de fuga: el Y2D negatiu del shape (la punta) passa a +Z mundial. - // Inclinem ~30° més enllà de -π/2 perquè el "cul" baixe i la punta puje; - // així la càmera (a Y=0, una mica per damunt de les naus) veu el dors. - constexpr float SHIP_PITCH_RAD = -2.0944F; // -120° - // Posicions en l'espai 3D (càmera a (0,0,0) mirant cap a +Z, Y cap amunt). constexpr float SHIP_FLOAT_X = 25.0F; // Separació horitzontal constexpr float SHIP_FLOAT_Y = -8.0F; // Lleugerament per davall del centre @@ -37,6 +31,43 @@ namespace Title { // Posició de fuga (al fons, centre projectat). constexpr float SHIP_EXIT_Z = 800.0F; + // Punt cap a on apunten les naus durant FLOATING i EXITING (centre lluny). + constexpr Vec3 VANISHING_POINT{.x = 0.0F, .y = 0.0F, .z = SHIP_EXIT_Z}; + + // Look-at: calcula pitch+yaw que duen (0,-1,0) local a forward_dir mundial. + // Requereix l'ordre de rotació X→Y→Z al applyTransform de wireframe3d. + // Si forward és quasi vertical (sin(pitch) ≈ 0), retorna yaw=0 (qualsevol). + auto computePitchYawForLookAt(const Vec3& forward_dir) -> Vec2 { + const float DY = std::clamp(forward_dir.y, -1.0F, 1.0F); + const float PITCH = -std::acos(-DY); // ∈ [-π, 0] + const float SIN_PITCH = std::sin(PITCH); + if (std::abs(SIN_PITCH) < 1.0E-5F) { + return Vec2{.x = PITCH, .y = 0.0F}; + } + const float SY = -forward_dir.x / SIN_PITCH; + const float CY = -forward_dir.z / SIN_PITCH; + return Vec2{.x = PITCH, .y = std::atan2(SY, CY)}; + } + + auto safeNormalize(const Vec3& v, const Vec3& fallback) -> Vec3 { + return v.lengthSquared() > 0.0F ? v.normalized() : fallback; + } + + auto entryForward(const TitleShip3D& ship) -> Vec3 { + return safeNormalize(ship.target_position - ship.initial_position, + Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F}); + } + + auto floatingForward(const Vec3& target) -> Vec3 { + return safeNormalize(VANISHING_POINT - target, + Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F}); + } + + auto exitForward(const Vec3& current) -> Vec3 { + return safeNormalize(VANISHING_POINT - current, + Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F}); + } + // Mida visual i animació. constexpr float SHIP_FLOAT_SCALE = 1.0F; constexpr float SHIP_ENTRY_SCALE = 1.0F; // Mida mundial idèntica; la perspectiva fa la resta @@ -107,9 +138,10 @@ namespace Title { if (!ship.visible) { continue; } + const Vec2 EULER = computePitchYawForLookAt(ship.forward_dir); const Graphics::Transform3D TRANSFORM{ .position = ship.current_position, - .rotation_euler = Vec3{.x = SHIP_PITCH_RAD, .y = 0.0F, .z = 0.0F}, + .rotation_euler = Vec3{.x = EULER.x, .y = EULER.y, .z = 0.0F}, .scale = ship.current_scale, }; Graphics::drawWireframe(renderer_, *camera_, ship.mesh, TRANSFORM, 1.0F); @@ -122,6 +154,7 @@ namespace Title { ship.state_time = 0.0F; ship.current_position = ship.initial_position; ship.current_scale = ship.initial_scale; + ship.forward_dir = entryForward(ship); } } @@ -130,6 +163,7 @@ namespace Title { ship.state = ShipState3D::EXITING; ship.state_time = 0.0F; ship.initial_position = ship.current_position; + ship.forward_dir = exitForward(ship.current_position); } } @@ -139,6 +173,7 @@ namespace Title { ship.state = ShipState3D::EXITING; ship.state_time = 0.0F; ship.initial_position = ship.current_position; + ship.forward_dir = exitForward(ship.current_position); break; } } @@ -151,6 +186,7 @@ namespace Title { ship.oscillation_phase = 0.0F; ship.current_position = ship.target_position; ship.current_scale = ship.target_scale; + ship.forward_dir = floatingForward(ship.target_position); } } @@ -207,6 +243,7 @@ namespace Title { ship.state_time = 0.0F; // No resetegem oscillation_phase: així updateFloating continua // l'oscil·lació iniciada durant ENTERING sense salt. + ship.forward_dir = floatingForward(ship.target_position); } } @@ -264,6 +301,7 @@ namespace Title { ship.amplitude_y = FLOAT_AMPLITUDE_Y; ship.frequency_x = FLOAT_FREQUENCY_X_BASE * P1_FREQUENCY_MULTIPLIER; ship.frequency_y = FLOAT_FREQUENCY_Y_BASE * P1_FREQUENCY_MULTIPLIER; + ship.forward_dir = entryForward(ship); ship.visible = true; } @@ -289,6 +327,7 @@ namespace Title { ship.amplitude_y = FLOAT_AMPLITUDE_Y; ship.frequency_x = FLOAT_FREQUENCY_X_BASE * P2_FREQUENCY_MULTIPLIER; ship.frequency_y = FLOAT_FREQUENCY_Y_BASE * P2_FREQUENCY_MULTIPLIER; + ship.forward_dir = entryForward(ship); ship.visible = true; } diff --git a/source/game/title/ship_animator3d.hpp b/source/game/title/ship_animator3d.hpp index 334a7b0..dee4177 100644 --- a/source/game/title/ship_animator3d.hpp +++ b/source/game/title/ship_animator3d.hpp @@ -46,6 +46,10 @@ namespace Title { float frequency_y{0.0F}; Graphics::Mesh3D mesh; + // Vector mundial cap a on apunta el front del shape. Recalculat a cada + // transició d'estat perquè draw() oriente la nau (look-at) en la + // direcció del seu path actual. + Vec3 forward_dir{.x = 0.0F, .y = 0.0F, .z = 1.0F}; bool visible{false}; };