diff --git a/CLAUDE.md b/CLAUDE.md index 62ac6cd..f6f6f21 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,9 +43,12 @@ - ✅ **Fibonacci Sphere Algorithm** - Distribución uniforme de puntos en esfera 3D - ✅ **Rotación dual (X/Y)** - Efecto visual dinámico estilo demoscene - ✅ **Profundidad Z simulada** - Color mod según distancia (oscuro=lejos, brillante=cerca) - - ✅ **Transición suave** - Interpolación 1.5s desde física a esfera + - ✅ **Física de atracción con resorte** - Sistema de fuerzas con conservación de momento + - ✅ **Transición física realista** - Pelotas atraídas a esfera rotante con aceleración + - ✅ **Amortiguación variable** - Mayor damping cerca del punto (estabilización) - ✅ **Sin sprites adicionales** - Usa SDL_SetTextureColorMod para profundidad - ✅ **Proyección ortográfica** - Coordenadas 3D → 2D en tiempo real + - ✅ **Conservación de inercia** - Al salir mantienen velocidad tangencial - ✅ **Compatible con temas** - Mantiene paleta de colores activa - ✅ **Performance optimizado** - Funciona con 1-100,000 pelotas @@ -326,6 +329,98 @@ float lerp_y = current_y + (target_sphere_y - current_y) * progress; - 10,000 pelotas: >100 FPS - 100,000 pelotas: >60 FPS (mismo que modo física) +--- + +## 🔬 Sistema de Física con Atracción (Spring Force) + +### Mejora Implementada: Transición Física Realista + +**Problema anterior:** Interpolación lineal artificial (lerp) sin física real +**Solución:** Sistema de resorte (Hooke's Law) con conservación de momento + +### Ecuaciones Implementadas + +#### Fuerza de Resorte (Ley de Hooke) +```cpp +F_spring = k * (target - position) +``` +- `k = 300.0`: Constante de rigidez del resorte (N/m) +- Mayor k = atracción más fuerte + +#### Fuerza de Amortiguación (Damping) +```cpp +F_damping = c * velocity +F_total = F_spring - F_damping +``` +- `c_base = 15.0`: Amortiguación lejos del punto +- `c_near = 50.0`: Amortiguación cerca (estabilización) +- Evita oscilaciones infinitas + +#### Aplicación de Fuerzas +```cpp +acceleration = F_total / mass // Asumiendo mass = 1 +velocity += acceleration * deltaTime +position += velocity * deltaTime +``` + +### Comportamiento Físico + +**Al activar RotoBall (tecla C):** +1. Esfera comienza a rotar inmediatamente +2. Cada pelota mantiene su velocidad actual (`vx`, `vy`) +3. Se aplica fuerza de atracción hacia punto móvil en esfera +4. Las pelotas se aceleran hacia sus destinos +5. Amortiguación las estabiliza al llegar + +**Durante RotoBall:** +- Punto destino rota constantemente (actualización cada frame) +- Fuerza se recalcula hacia posición rotada +- Pelotas "persiguen" su punto mientras este se mueve +- Efecto: Convergencia con ligera oscilación orbital + +**Al desactivar RotoBall (tecla C):** +1. Atracción se desactiva (`enableRotoBallAttraction(false)`) +2. Pelotas conservan velocidad tangencial actual +3. Gravedad vuelve a aplicarse +4. Transición suave a física normal + +### Constantes Físicas Ajustables + +```cpp +// En defines.h +ROTOBALL_SPRING_K = 300.0f; // Rigidez resorte +ROTOBALL_DAMPING_BASE = 15.0f; // Amortiguación lejos +ROTOBALL_DAMPING_NEAR = 50.0f; // Amortiguación cerca +ROTOBALL_NEAR_THRESHOLD = 5.0f; // Distancia "cerca" (px) +ROTOBALL_MAX_FORCE = 1000.0f; // Límite fuerza (seguridad) +``` + +### Ajustes Recomendados + +**Si oscilan demasiado:** +```cpp +ROTOBALL_DAMPING_BASE = 25.0f; // Más amortiguación +ROTOBALL_DAMPING_NEAR = 70.0f; // Estabilización fuerte +``` + +**Si tardan en llegar:** +```cpp +ROTOBALL_SPRING_K = 500.0f; // Resorte más rígido +``` + +**Si se "pegan" muy rápido (sin inercia visible):** +```cpp +ROTOBALL_DAMPING_NEAR = 30.0f; // Menos amortiguación cerca +``` + +### Ventajas del Sistema + +✅ **Física realista**: Conservación de momento angular +✅ **Transición orgánica**: Aceleración natural, no artificial +✅ **Inercia preservada**: Al salir conservan velocidad +✅ **Estabilización automática**: Damping evita oscilaciones infinitas +✅ **Performance**: O(1) por pelota, muy eficiente + ## Métricas del Proyecto ### ✅ Logros Actuales diff --git a/source/ball.cpp b/source/ball.cpp index 81e6cbd..fe0dc61 100644 --- a/source/ball.cpp +++ b/source/ball.cpp @@ -50,6 +50,7 @@ Ball::Ball(float x, float vx, float vy, Color color, std::shared_ptr te target_x_ = pos_.x; target_y_ = pos_.y; depth_brightness_ = 1.0f; + rotoball_attraction_active_ = false; } // Actualiza la lógica de la clase @@ -277,4 +278,64 @@ void Ball::setRotoBallScreenPosition(float x, float y) { void Ball::setDepthBrightness(float brightness) { depth_brightness_ = brightness; +} + +// Activar/desactivar atracción física hacia esfera RotoBall +void Ball::enableRotoBallAttraction(bool enable) { + rotoball_attraction_active_ = enable; +} + +// Aplicar fuerza de resorte hacia punto objetivo en esfera rotante +void Ball::applyRotoBallForce(float target_x, float target_y, float deltaTime) { + if (!rotoball_attraction_active_) return; + + // Calcular vector diferencia (dirección hacia el target) + float diff_x = target_x - pos_.x; + float diff_y = target_y - pos_.y; + + // Calcular distancia al punto objetivo + float distance = sqrtf(diff_x * diff_x + diff_y * diff_y); + + // Fuerza de resorte (Ley de Hooke: F = -k * x) + float spring_force_x = ROTOBALL_SPRING_K * diff_x; + float spring_force_y = ROTOBALL_SPRING_K * diff_y; + + // Amortiguación variable: más cerca del punto = más amortiguación (estabilización) + float damping = (distance < ROTOBALL_NEAR_THRESHOLD) + ? ROTOBALL_DAMPING_NEAR + : ROTOBALL_DAMPING_BASE; + + // Fuerza de amortiguación (proporcional a la velocidad) + float damping_force_x = damping * vx_; + float damping_force_y = damping * vy_; + + // Fuerza total = Resorte - Amortiguación + float total_force_x = spring_force_x - damping_force_x; + float total_force_y = spring_force_y - damping_force_y; + + // Limitar magnitud de fuerza (evitar explosiones numéricas) + float force_magnitude = sqrtf(total_force_x * total_force_x + total_force_y * total_force_y); + if (force_magnitude > ROTOBALL_MAX_FORCE) { + float scale = ROTOBALL_MAX_FORCE / force_magnitude; + total_force_x *= scale; + total_force_y *= scale; + } + + // Aplicar aceleración (F = ma, asumiendo m = 1 para simplificar) + // a = F/m, pero m=1, así que a = F + vx_ += total_force_x * deltaTime; + vy_ += total_force_y * deltaTime; + + // Actualizar posición con física normal (velocidad integrada) + pos_.x += vx_ * deltaTime; + pos_.y += vy_ * deltaTime; + + // Mantener pelotas dentro de los límites de pantalla + if (pos_.x < 0) pos_.x = 0; + if (pos_.x + pos_.w > screen_width_) pos_.x = screen_width_ - pos_.w; + if (pos_.y < 0) pos_.y = 0; + if (pos_.y + pos_.h > screen_height_) pos_.y = screen_height_ - pos_.h; + + // Actualizar sprite para renderizado + sprite_->setPos({pos_.x, pos_.y}); } \ No newline at end of file diff --git a/source/ball.h b/source/ball.h index a781200..479332a 100644 --- a/source/ball.h +++ b/source/ball.h @@ -27,6 +27,7 @@ class Ball { float pos_3d_x_, pos_3d_y_, pos_3d_z_; // Posición 3D en la esfera float target_x_, target_y_; // Posición destino 2D (proyección) float depth_brightness_; // Brillo según profundidad Z (0.0-1.0) + bool rotoball_attraction_active_; // ¿Está siendo atraída hacia la esfera? public: // Constructor @@ -72,4 +73,8 @@ class Ball { void setRotoBallScreenPosition(float x, float y); // Establecer posición directa en pantalla void setDepthBrightness(float brightness); float getDepthBrightness() const { return depth_brightness_; } + + // Sistema de atracción física hacia esfera RotoBall + void enableRotoBallAttraction(bool enable); + void applyRotoBallForce(float target_x, float target_y, float deltaTime); }; \ No newline at end of file diff --git a/source/defines.h b/source/defines.h index 14486e1..e481840 100644 --- a/source/defines.h +++ b/source/defines.h @@ -71,4 +71,12 @@ constexpr float ROTOBALL_ROTATION_SPEED_X = 0.8f; // Velocidad rotación eje X constexpr float ROTOBALL_TRANSITION_TIME = 1.5f; // Tiempo de transición (segundos) constexpr int ROTOBALL_MIN_BRIGHTNESS = 50; // Brillo mínimo (fondo, 0-255) constexpr int ROTOBALL_MAX_BRIGHTNESS = 255; // Brillo máximo (frente, 0-255) + +// Física de atracción RotoBall (sistema de resorte) +constexpr float ROTOBALL_SPRING_K = 300.0f; // Constante de rigidez del resorte (N/m) +constexpr float ROTOBALL_DAMPING_BASE = 15.0f; // Amortiguación base (lejos del punto) +constexpr float ROTOBALL_DAMPING_NEAR = 50.0f; // Amortiguación cerca del punto (estabilización) +constexpr float ROTOBALL_NEAR_THRESHOLD = 5.0f; // Distancia "cerca" en píxeles +constexpr float ROTOBALL_MAX_FORCE = 1000.0f; // Fuerza máxima aplicable (evita explosiones) + constexpr float PI = 3.14159265358979323846f; // Constante PI \ No newline at end of file diff --git a/source/engine.cpp b/source/engine.cpp index 39e7039..b4bca1b 100644 --- a/source/engine.cpp +++ b/source/engine.cpp @@ -861,6 +861,12 @@ void Engine::toggleRotoBallMode() { // Generar esfera 3D generateRotoBallSphere(); + // Activar atracción física en todas las pelotas + // Las pelotas mantienen su velocidad actual y son atraídas hacia la esfera + for (auto& ball : balls_) { + ball->enableRotoBallAttraction(true); + } + // Mostrar texto informativo text_ = "MODO ROTOBALL"; int text_width = static_cast(text_.length() * 8); @@ -868,11 +874,16 @@ void Engine::toggleRotoBallMode() { text_init_time_ = SDL_GetTicks(); show_text_ = true; } else { - // Volver a modo física + // Volver a modo física normal current_mode_ = SimulationMode::PHYSICS; rotoball_.transitioning = false; rotoball_.transition_progress = 0.0f; + // Desactivar atracción - las pelotas conservan su velocidad tangencial actual + for (auto& ball : balls_) { + ball->enableRotoBallAttraction(false); + } + // Mostrar texto informativo text_ = "MODO FISICA"; int text_width = static_cast(text_.length() * 8); @@ -916,20 +927,11 @@ void Engine::generateRotoBallSphere() { } } -// Actualizar esfera RotoBall (rotación + proyección) +// Actualizar esfera RotoBall con física de atracción void Engine::updateRotoBall() { if (current_mode_ != SimulationMode::ROTOBALL) return; - // Actualizar transición si está activa - if (rotoball_.transitioning) { - rotoball_.transition_progress += delta_time_ / ROTOBALL_TRANSITION_TIME; - if (rotoball_.transition_progress >= 1.0f) { - rotoball_.transition_progress = 1.0f; - rotoball_.transitioning = false; - } - } - - // Actualizar ángulos de rotación + // Actualizar ángulos de rotación de la esfera rotoball_.angle_y += ROTOBALL_ROTATION_SPEED_Y * delta_time_; rotoball_.angle_x += ROTOBALL_ROTATION_SPEED_X * delta_time_; @@ -937,15 +939,8 @@ void Engine::updateRotoBall() { float center_x = current_screen_width_ / 2.0f; float center_y = current_screen_height_ / 2.0f; - // Actualizar cada pelota + // Actualizar cada pelota con física de atracción for (size_t i = 0; i < balls_.size(); i++) { - // Obtener posición 3D original (almacenada en generateRotoBallSphere) - SDL_FRect pos = balls_[i]->getPosition(); - - // Reconstruir coordenadas 3D originales desde los datos almacenados - // En generateRotoBallSphere guardamos: x, y, z en setRotoBallPosition3D - // Pero necesitamos acceder a esos datos... por ahora recalcularemos - // Recalcular posición 3D original usando Fibonacci sphere int num_points = static_cast(balls_.size()); const float golden_ratio = (1.0f + sqrtf(5.0f)) / 2.0f; @@ -971,26 +966,16 @@ void Engine::updateRotoBall() { float y_rot = y * cos_x - z_rot * sin_x; float z_final = y * sin_x + z_rot * cos_x; - // Proyección 2D ortográfica - float screen_x = center_x + x_rot; - float screen_y = center_y + y_rot; + // Proyección 2D ortográfica (punto objetivo móvil) + float target_x = center_x + x_rot; + float target_y = center_y + y_rot; - // Calcular brillo según profundidad Z (normalizado 0-1) + // Aplicar fuerza de atracción física hacia el punto rotado + balls_[i]->applyRotoBallForce(target_x, target_y, delta_time_); + + // Calcular brillo según profundidad Z para renderizado float z_normalized = (z_final + ROTOBALL_RADIUS) / (2.0f * ROTOBALL_RADIUS); z_normalized = std::max(0.0f, std::min(1.0f, z_normalized)); - - // Guardar brillo para usar en render balls_[i]->setDepthBrightness(z_normalized); - - // Transición suave desde posición actual a posición de esfera - if (rotoball_.transitioning) { - // Interpolar desde posición actual hacia posición de esfera - float lerp_x = pos.x + (screen_x - pos.x) * rotoball_.transition_progress; - float lerp_y = pos.y + (screen_y - pos.y) * rotoball_.transition_progress; - balls_[i]->setRotoBallScreenPosition(lerp_x, lerp_y); - } else { - // Ya en esfera, actualizar directamente - balls_[i]->setRotoBallScreenPosition(screen_x, screen_y); - } } } \ No newline at end of file