Compare commits
208 Commits
31ac22bd0f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 44509023dc | |||
| 2d2e338c7a | |||
| e5fdbd54ff | |||
| c9bcce6f9b | |||
| 4801f287df | |||
| 7af77fb3dd | |||
| 96dc964d6a | |||
| 2846987450 | |||
| e13905567d | |||
| 9ae851d5b6 | |||
| e1f6fd0f39 | |||
| 093b982e01 | |||
| 74d954df1e | |||
| 46b24bf075 | |||
| 33cb995872 | |||
| c40eb69fc1 | |||
| 1d2e9c5035 | |||
| f71f7cd5ed | |||
| dcea4ebbab | |||
| b9b5f0b29f | |||
| 200672756c | |||
| f3b029c5b6 | |||
| e46c3eb4ba | |||
| ea05e1eb2e | |||
| c052b45a60 | |||
| 8dde13409b | |||
| 5b4a970157 | |||
| f272bab296 | |||
| e3f29c864b | |||
| d76c7f75a2 | |||
|
|
0678a38a32 | ||
| 6ffe7594ab | |||
| 50926df97c | |||
| 5c0d0479ad | |||
| a51072db32 | |||
| d2e7f2ff86 | |||
| badf92420b | |||
| 310c6d244e | |||
| af0276255e | |||
| 00a5875c92 | |||
| 736db8cf41 | |||
| 821eba3483 | |||
| 6409b61bd5 | |||
| 2d1f4195dc | |||
| 18a6735a39 | |||
| 83934f9507 | |||
| e8a6485e88 | |||
| ec0f14afd8 | |||
| 6aa4a1227e | |||
| 02fdcd4113 | |||
| 7db9e46f95 | |||
| ff6aaef7c6 | |||
| 8e2e681b2c | |||
| f06123feff | |||
| cbe6dc9744 | |||
| dfbd8a430b | |||
| ea27a771ab | |||
| 09303537a4 | |||
| df17e85a8a | |||
| ce5c4681b8 | |||
| b79f1c3424 | |||
| a65544e8b3 | |||
| b9264c96a1 | |||
| fa285519b2 | |||
| 8285a8fafe | |||
| 1a555e03f7 | |||
| af3ed6c2b3 | |||
| a9d7b66e83 | |||
| a929df6b73 | |||
| 3f027d953c | |||
| 1354ed82d2 | |||
| 2fd6d99a61 | |||
| 2fa1684f01 | |||
| 41c76316ef | |||
| ce50a29019 | |||
| f25cb96a91 | |||
| d73781be9f | |||
| 288e4813e8 | |||
| 4d3ddec14e | |||
| ec1700b439 | |||
| 8aa2a112b4 | |||
| dfebd8ece4 | |||
| 827d9f0e76 | |||
| df93d5080d | |||
| 0da4b45fef | |||
| db8acf0331 | |||
| 5a35cc1abf | |||
| d30a4fd440 | |||
| 97c0683f6e | |||
| c3d24cc07d | |||
| 7609b9ef5c | |||
| ad3f5a00e4 | |||
| c91cb1ca56 | |||
| 8d608357b4 | |||
| f73a133756 | |||
| de23327861 | |||
| f6402084eb | |||
| 9909d4c12d | |||
| a929346463 | |||
| c4075f68db | |||
| 399650f8da | |||
| 9b8afa1219 | |||
| 5b674c8ea6 | |||
| 7fac103c51 | |||
| bcceb94c9e | |||
| 1b3d32ba84 | |||
| 7c0a60f140 | |||
| 250b1a640d | |||
| 795fa33e50 | |||
| e7dc8f6d13 | |||
| 9cabbd867f | |||
| 8c2a8857fc | |||
| 3d26bfc6fa | |||
| adfa315a43 | |||
| 18a8812ad7 | |||
| 35f29340db | |||
| abbda0f30b | |||
| 6aacb86d6a | |||
| 0873d80765 | |||
| b73e77e9bc | |||
| 1bb8807060 | |||
| 39c0a24a45 | |||
| 01d1ebd2a3 | |||
| 83ea03fda3 | |||
| d62b8e5f52 | |||
| 0fe2efc051 | |||
| 1c38ab2009 | |||
| 8be4c5586d | |||
| e4636c8e82 | |||
| e2a60e4f87 | |||
| e655c643a5 | |||
| f93879b803 | |||
| b8d3c60e58 | |||
| 5f89299444 | |||
| 33728857ac | |||
| d2f170d313 | |||
| aa57ac7012 | |||
| 0d069da29d | |||
| eb3dd03579 | |||
| a1e2c03efd | |||
| 684ac9823b | |||
| 82e5b6798c | |||
| d93ac04ee3 | |||
| 10a4234d49 | |||
| 0d1608712b | |||
| 68381dc92d | |||
| f00b08b6be | |||
| c50ecbc02a | |||
| f1bafc8a4f | |||
| 0592699a0b | |||
| a134ae428f | |||
| b93028396a | |||
| 6cb3c2eef9 | |||
| c55d6de687 | |||
| 77a585092d | |||
| ebeec288ee | |||
| 871bdf49ce | |||
| 9a6cfdaaeb | |||
| 38b8789884 | |||
| 9390bd3b01 | |||
| a7c9304214 | |||
| f41fbb6e6b | |||
| 2f0abbb436 | |||
| 597f26461a | |||
| f5d6c993d3 | |||
| 2d405a86d7 | |||
| c9db7e6038 | |||
| 577fe843f9 | |||
| 2cd585ece0 | |||
| ef2f5bea01 | |||
| 042c3cad1a | |||
| 4f900eaa57 | |||
| be099c198c | |||
| f0baa51415 | |||
| db3d4d6630 | |||
| d030d4270e | |||
| fbd09b3201 | |||
| a04c1cba13 | |||
| 757bb9c525 | |||
| 723bb6d198 | |||
| 1e5c9f8f9d | |||
| e24f06ed90 | |||
| c4ca49b006 | |||
| 0f0617066e | |||
| 9eb03b5091 | |||
| 0d49a6e814 | |||
| d0b144dddc | |||
| 06aabc53c0 | |||
| 2ae515592d | |||
| af3274e9bc | |||
| 59c5ebe9be | |||
| 3be3833e55 | |||
| 3e83e51e2d | |||
| 6980d4e876 | |||
| dcd05e502f | |||
| 6bb814e61c | |||
| 95ab6dea46 | |||
| 5391e0cddf | |||
| fb788666cc | |||
| ac3309ffd1 | |||
| 8b642f6903 | |||
| bcbeaba504 | |||
| 8cf117ea64 | |||
| 91f8bfdd30 | |||
| a484ce69e8 | |||
| a7ec764ebc | |||
| b196683e4a | |||
| 535c397be2 |
@@ -16,6 +16,7 @@ Checks: >
|
|||||||
-performance-inefficient-string-concatenation,
|
-performance-inefficient-string-concatenation,
|
||||||
-bugprone-integer-division,
|
-bugprone-integer-division,
|
||||||
-bugprone-easily-swappable-parameters,
|
-bugprone-easily-swappable-parameters,
|
||||||
|
-readability-uppercase-literal-suffix,
|
||||||
|
|
||||||
WarningsAsErrors: '*'
|
WarningsAsErrors: '*'
|
||||||
# Solo incluir archivos de tu código fuente
|
# Solo incluir archivos de tu código fuente
|
||||||
|
|||||||
26
.gitignore
vendored
@@ -12,7 +12,6 @@ vibe3_physics.exe
|
|||||||
*.lib
|
*.lib
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
*.dll
|
|
||||||
|
|
||||||
# Archivos de compilación y enlazado
|
# Archivos de compilación y enlazado
|
||||||
*.d
|
*.d
|
||||||
@@ -26,6 +25,7 @@ Build/
|
|||||||
BUILD/
|
BUILD/
|
||||||
cmake-build-*/
|
cmake-build-*/
|
||||||
.cmake/
|
.cmake/
|
||||||
|
.cache/
|
||||||
|
|
||||||
# Archivos generados por CMake
|
# Archivos generados por CMake
|
||||||
CMakeFiles/
|
CMakeFiles/
|
||||||
@@ -57,7 +57,6 @@ Makefile
|
|||||||
moc_*.cpp
|
moc_*.cpp
|
||||||
moc_*.h
|
moc_*.h
|
||||||
qrc_*.cpp
|
qrc_*.cpp
|
||||||
ui_*.h
|
|
||||||
*.qm
|
*.qm
|
||||||
.qmake.stash
|
.qmake.stash
|
||||||
|
|
||||||
@@ -78,7 +77,6 @@ Thumbs.db
|
|||||||
*~
|
*~
|
||||||
|
|
||||||
# Archivos de IDEs
|
# Archivos de IDEs
|
||||||
.vscode/
|
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
@@ -94,4 +92,24 @@ Thumbs.db
|
|||||||
*.temp
|
*.temp
|
||||||
|
|
||||||
# Claude Code
|
# Claude Code
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
# Archivos de recursos empaquetados
|
||||||
|
resources.pack
|
||||||
|
|
||||||
|
# Archivos de distribución (resultados de release)
|
||||||
|
*.zip
|
||||||
|
*.dmg
|
||||||
|
*.tar.gz
|
||||||
|
*.AppImage
|
||||||
|
|
||||||
|
# Carpetas temporales de empaquetado
|
||||||
|
vibe3_release/
|
||||||
|
Frameworks/
|
||||||
|
|
||||||
|
# Carpeta de distribución generada
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Binarios de herramientas
|
||||||
|
tools/pack_resources
|
||||||
|
tools/*.exe
|
||||||
60
.vscode/c_cpp_properties.json
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "macOS",
|
||||||
|
"includePath": [
|
||||||
|
"${workspaceFolder}/source",
|
||||||
|
"${workspaceFolder}/source/external",
|
||||||
|
"${workspaceFolder}/build/generated_shaders",
|
||||||
|
"${env:HOMEBREW_PREFIX}/include",
|
||||||
|
"/opt/homebrew/include"
|
||||||
|
],
|
||||||
|
"defines": [
|
||||||
|
"MACOS_BUILD"
|
||||||
|
],
|
||||||
|
"macFrameworkPath": [
|
||||||
|
"/System/Library/Frameworks",
|
||||||
|
"/Library/Frameworks"
|
||||||
|
],
|
||||||
|
"compilerPath": "/usr/bin/clang++",
|
||||||
|
"cStandard": "c17",
|
||||||
|
"cppStandard": "c++20",
|
||||||
|
"intelliSenseMode": "macos-clang-arm64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Linux",
|
||||||
|
"includePath": [
|
||||||
|
"${workspaceFolder}/source",
|
||||||
|
"${workspaceFolder}/source/external",
|
||||||
|
"${workspaceFolder}/build/generated_shaders",
|
||||||
|
"/usr/include",
|
||||||
|
"/usr/local/include"
|
||||||
|
],
|
||||||
|
"defines": [
|
||||||
|
"LINUX_BUILD"
|
||||||
|
],
|
||||||
|
"compilerPath": "/usr/bin/g++",
|
||||||
|
"cStandard": "c17",
|
||||||
|
"cppStandard": "c++20",
|
||||||
|
"intelliSenseMode": "linux-gcc-x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Win32",
|
||||||
|
"includePath": [
|
||||||
|
"${workspaceFolder}/source",
|
||||||
|
"${workspaceFolder}/source/external",
|
||||||
|
"${workspaceFolder}/build/generated_shaders",
|
||||||
|
"C:/mingw/include"
|
||||||
|
],
|
||||||
|
"defines": [
|
||||||
|
"WINDOWS_BUILD",
|
||||||
|
"_WIN32"
|
||||||
|
],
|
||||||
|
"compilerPath": "C:/mingw/bin/g++.exe",
|
||||||
|
"cStandard": "c17",
|
||||||
|
"cppStandard": "c++20",
|
||||||
|
"intelliSenseMode": "windows-gcc-x64"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": 4
|
||||||
|
}
|
||||||
547
CLAUDE.md
@@ -1,547 +0,0 @@
|
|||||||
# Claude Code Session - ViBe3 Physics
|
|
||||||
|
|
||||||
## Estado del Proyecto
|
|
||||||
|
|
||||||
**Proyecto:** ViBe3 Physics - Simulador de sprites con físicas avanzadas
|
|
||||||
**Objetivo:** Implementar nuevas físicas experimentales expandiendo sobre el sistema de delta time
|
|
||||||
**Base:** Migrado desde vibe1_delta con sistema delta time ya implementado
|
|
||||||
|
|
||||||
## Progreso Actual
|
|
||||||
|
|
||||||
### ✅ Completado
|
|
||||||
|
|
||||||
#### 1. **Migración y Setup Inicial**
|
|
||||||
- ✅ Renombrado vibe1_delta → vibe3_physics en todos los archivos
|
|
||||||
- ✅ Carpeta resources → data
|
|
||||||
- ✅ Actualizado CMakeLists.txt, .gitignore, defines.h, README.md
|
|
||||||
- ✅ Añadido .claude/ al .gitignore
|
|
||||||
- ✅ Sistema de compilación CMake funcionando
|
|
||||||
|
|
||||||
#### 2. **Sistema de Físicas Base (Heredado)**
|
|
||||||
- ✅ **Delta time implementado** - Física independiente del framerate
|
|
||||||
- ✅ Contador FPS en tiempo real (esquina superior derecha, amarillo)
|
|
||||||
- ✅ Control V-Sync dinámico con tecla "V" (ON/OFF)
|
|
||||||
- ✅ Display V-Sync (esquina superior izquierda, cian)
|
|
||||||
- ✅ **Sistema de temas visuales** - 4 temas (SUNSET/OCEAN/NEON/FOREST)
|
|
||||||
- ✅ **Batch rendering optimizado** - Maneja hasta 100,000 sprites
|
|
||||||
|
|
||||||
#### 3. **NUEVA CARACTERÍSTICA: Gravedad Direccional** 🎯
|
|
||||||
- ✅ **Enum GravityDirection** (UP/DOWN/LEFT/RIGHT) en defines.h
|
|
||||||
- ✅ **Ball class actualizada** para física multi-direccional
|
|
||||||
- ✅ **Detección de superficie inteligente** - Adaptada a cada dirección
|
|
||||||
- ✅ **Fricción direccional** - Se aplica en la superficie correcta
|
|
||||||
- ✅ **Controles de cursor** - Cambio dinámico de gravedad
|
|
||||||
- ✅ **Debug display actualizado** - Muestra dirección actual
|
|
||||||
|
|
||||||
#### 4. **NUEVA CARACTERÍSTICA: Coeficientes de Rebote Variables** ⚡
|
|
||||||
- ✅ **Rango ampliado** - De 0.60-0.89 a 0.30-0.95 (+120% variabilidad)
|
|
||||||
- ✅ **Comportamientos diversos** - Desde pelotas super rebotonas a muy amortiguadas
|
|
||||||
- ✅ **Debug display** - Muestra coeficiente LOSS de primera pelota
|
|
||||||
- ✅ **Física realista** - Elimina sincronización entre pelotas
|
|
||||||
|
|
||||||
#### 5. **🎯 NUEVA CARACTERÍSTICA: Modo RotoBall (Esfera 3D Rotante)** 🌐
|
|
||||||
- ✅ **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)
|
|
||||||
- ✅ **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
|
|
||||||
|
|
||||||
### 📋 Controles Actuales
|
|
||||||
|
|
||||||
| Tecla | Acción |
|
|
||||||
|-------|--------|
|
|
||||||
| **↑** | **Gravedad hacia ARRIBA** |
|
|
||||||
| **↓** | **Gravedad hacia ABAJO** |
|
|
||||||
| **←** | **Gravedad hacia IZQUIERDA** |
|
|
||||||
| **→** | **Gravedad hacia DERECHA** |
|
|
||||||
| **C** | **🌐 MODO ROTOBALL - Toggle esfera 3D rotante** |
|
|
||||||
| V | Alternar V-Sync ON/OFF |
|
|
||||||
| H | **Toggle debug display (FPS, V-Sync, física, gravedad, modo)** |
|
|
||||||
| Num 1-5 | Selección directa de tema (1-Atardecer/2-Océano/3-Neón/4-Bosque/5-RGB) |
|
|
||||||
| T | Ciclar entre temas de colores |
|
|
||||||
| 1-8 | Cambiar número de pelotas (1 a 100,000) |
|
|
||||||
| ESPACIO | Impulsar pelotas hacia arriba |
|
|
||||||
| G | Alternar gravedad ON/OFF (mantiene dirección) |
|
|
||||||
| ESC | Salir |
|
|
||||||
|
|
||||||
### 🎯 Debug Display (Tecla H)
|
|
||||||
|
|
||||||
Cuando está activado muestra:
|
|
||||||
```
|
|
||||||
FPS: 75 # Esquina superior derecha (amarillo)
|
|
||||||
VSYNC ON # Esquina superior izquierda (cian)
|
|
||||||
GRAV 720 # Magnitud gravedad (magenta)
|
|
||||||
VY -145 # Velocidad Y primera pelota (magenta)
|
|
||||||
SURFACE YES # En superficie (magenta)
|
|
||||||
LOSS 0.73 # Coeficiente rebote primera pelota (magenta)
|
|
||||||
GRAVITY DOWN # Dirección actual (amarillo)
|
|
||||||
THEME SUNSET # Tema activo (amarillo claro)
|
|
||||||
MODE PHYSICS # Modo simulación actual (verde claro) - PHYSICS/ROTOBALL
|
|
||||||
```
|
|
||||||
|
|
||||||
## Arquitectura Actual
|
|
||||||
|
|
||||||
```
|
|
||||||
vibe3_physics/
|
|
||||||
├── source/
|
|
||||||
│ ├── main.cpp # Bucle principal + controles + debug
|
|
||||||
│ ├── ball.h/.cpp # Clase Ball con física direccional
|
|
||||||
│ ├── defines.h # Constantes + enum GravityDirection
|
|
||||||
│ └── external/ # Utilidades externas
|
|
||||||
│ ├── texture.h/.cpp # Gestión texturas + nearest filter
|
|
||||||
│ ├── sprite.h/.cpp # Sistema sprites
|
|
||||||
│ ├── dbgtxt.h # Debug text + nearest filter
|
|
||||||
│ └── stb_image.h # Carga imágenes
|
|
||||||
├── data/ # Recursos (antes resources/)
|
|
||||||
│ └── ball.png # Textura pelota 10x10px
|
|
||||||
├── CMakeLists.txt # Build system
|
|
||||||
└── CLAUDE.md # Este archivo de seguimiento
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sistema de Gravedad Direccional
|
|
||||||
|
|
||||||
### 🔧 Implementación Técnica
|
|
||||||
|
|
||||||
#### Enum y Estados
|
|
||||||
```cpp
|
|
||||||
enum class GravityDirection {
|
|
||||||
DOWN, // ↓ Gravedad hacia abajo (por defecto)
|
|
||||||
UP, // ↑ Gravedad hacia arriba
|
|
||||||
LEFT, // ← Gravedad hacia la izquierda
|
|
||||||
RIGHT // → Gravedad hacia la derecha
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Lógica de Física por Dirección
|
|
||||||
- **DOWN**: Pelotas caen hacia abajo, fricción en suelo inferior
|
|
||||||
- **UP**: Pelotas "caen" hacia arriba, fricción en techo
|
|
||||||
- **LEFT**: Pelotas "caen" hacia izquierda, fricción en pared izquierda
|
|
||||||
- **RIGHT**: Pelotas "caen" hacia derecha, fricción en pared derecha
|
|
||||||
|
|
||||||
#### Cambios en Ball Class
|
|
||||||
- `on_floor_` → `on_surface_` (más genérico)
|
|
||||||
- `gravity_direction_` (nuevo miembro)
|
|
||||||
- `setGravityDirection()` (nueva función)
|
|
||||||
- `update()` completamente reescrito para lógica direccional
|
|
||||||
|
|
||||||
## Lecciones Aprendidas de ViBe2 Modules
|
|
||||||
|
|
||||||
### ✅ Éxitos de Modularización
|
|
||||||
- **C++20 modules** son viables para código propio
|
|
||||||
- **CMake + Ninja** funciona bien para modules
|
|
||||||
- **Separación clara** de responsabilidades mejora arquitectura
|
|
||||||
|
|
||||||
### ❌ Limitaciones Encontradas
|
|
||||||
- **SDL3 + modules** generan conflictos irresolubles
|
|
||||||
- **Bibliotecas externas** requieren includes tradicionales
|
|
||||||
- **Enfoque híbrido** (modules propios + includes externos) es más práctico
|
|
||||||
|
|
||||||
### 🎯 Decisión para ViBe3 Physics
|
|
||||||
- **Headers tradicionales** (.h/.cpp) por compatibilidad
|
|
||||||
- **Enfoque en características** antes que arquitectura
|
|
||||||
- **Organización por clases** en lugar de modules inicialmente
|
|
||||||
|
|
||||||
## Sistema de Coeficientes de Rebote Variables
|
|
||||||
|
|
||||||
### 🔧 Implementación Técnica
|
|
||||||
|
|
||||||
#### Problema Anterior
|
|
||||||
```cpp
|
|
||||||
// Sistema ANTIGUO - Poca variabilidad
|
|
||||||
loss_ = ((rand() % 30) * 0.01f) + 0.6f; // 0.60 - 0.89 (diferencia: 0.29)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Resultado**: Pelotas con comportamientos muy similares → Sincronización visible
|
|
||||||
|
|
||||||
#### Solución Implementada
|
|
||||||
```cpp
|
|
||||||
// Sistema NUEVO - Alta variabilidad
|
|
||||||
loss_ = ((rand() % 66) * 0.01f) + 0.30f; // 0.30 - 0.95 (diferencia: 0.65)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🎯 Tipos de Comportamiento
|
|
||||||
|
|
||||||
#### Categorías de Materiales
|
|
||||||
- **🏀 Super Rebotona** (0.85-0.95): Casi no pierde energía, rebota muchas veces
|
|
||||||
- **⚽ Normal** (0.65-0.85): Comportamiento estándar equilibrado
|
|
||||||
- **🎾 Amortiguada** (0.45-0.65): Pierde energía moderada, se estabiliza
|
|
||||||
- **🏐 Muy Amortiguada** (0.30-0.45): Se para rápidamente, pocas rebotes
|
|
||||||
|
|
||||||
### ✅ Beneficios Conseguidos
|
|
||||||
- **+120% variabilidad** en coeficientes de rebote
|
|
||||||
- **Eliminación de sincronización** entre pelotas
|
|
||||||
- **Comportamientos diversos** visibles inmediatamente
|
|
||||||
- **Física más realista** con materiales diferentes
|
|
||||||
- **Debug display** para monitoreo en tiempo real
|
|
||||||
|
|
||||||
## 🚀 Próximos Pasos - Físicas Avanzadas
|
|
||||||
|
|
||||||
### Ideas Pendientes de Implementación
|
|
||||||
|
|
||||||
#### 1. **Colisiones Entre Partículas**
|
|
||||||
- Detección de colisión ball-to-ball
|
|
||||||
- Física de rebotes entre pelotas
|
|
||||||
- Conservación de momentum
|
|
||||||
|
|
||||||
#### 2. **Materiales y Propiedades**
|
|
||||||
- Diferentes coeficientes de rebote por pelota
|
|
||||||
- Fricción variable por material
|
|
||||||
- Densidad y masa como propiedades
|
|
||||||
|
|
||||||
#### 3. **Fuerzas Externas**
|
|
||||||
- **Viento** - Fuerza horizontal constante
|
|
||||||
- **Campos magnéticos** - Atracción/repulsión a puntos
|
|
||||||
- **Turbulencia** - Fuerzas aleatorias localizadas
|
|
||||||
|
|
||||||
#### 4. **Interactividad Avanzada**
|
|
||||||
- Click para aplicar fuerzas puntuales
|
|
||||||
- Arrastrar para crear campos de fuerza
|
|
||||||
- Herramientas de "pincel" de física
|
|
||||||
|
|
||||||
#### 5. **Visualización Avanzada**
|
|
||||||
- **Trails** - Estelas de movimiento
|
|
||||||
- **Heatmaps** - Visualización de velocidad/energía
|
|
||||||
- **Vectores de fuerza** - Visualizar gravedad y fuerzas
|
|
||||||
|
|
||||||
#### 6. **Optimizaciones**
|
|
||||||
- Spatial partitioning para colisiones
|
|
||||||
- Level-of-detail para muchas partículas
|
|
||||||
- GPU compute shaders para física masiva
|
|
||||||
|
|
||||||
### 🎮 Controles Futuros Sugeridos
|
|
||||||
```
|
|
||||||
Mouse Click: Aplicar fuerza puntual
|
|
||||||
Mouse Drag: Crear campo de fuerza
|
|
||||||
Mouse Wheel: Ajustar intensidad
|
|
||||||
R: Reset todas las pelotas
|
|
||||||
P: Pausa/Resume física
|
|
||||||
M: Modo materiales
|
|
||||||
W: Toggle viento
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🌐 Implementación Técnica: Modo RotoBall
|
|
||||||
|
|
||||||
### Algoritmo Fibonacci Sphere
|
|
||||||
|
|
||||||
Distribución uniforme de puntos en una esfera usando la secuencia de Fibonacci:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
const float golden_ratio = (1.0f + sqrtf(5.0f)) / 2.0f;
|
|
||||||
const float angle_increment = PI * 2.0f * golden_ratio;
|
|
||||||
|
|
||||||
for (int i = 0; i < num_points; i++) {
|
|
||||||
float t = static_cast<float>(i) / static_cast<float>(num_points);
|
|
||||||
float phi = acosf(1.0f - 2.0f * t); // Latitud: 0 a π
|
|
||||||
float theta = angle_increment * i; // Longitud: 0 a 2π * golden_ratio
|
|
||||||
|
|
||||||
// Coordenadas esféricas → cartesianas
|
|
||||||
float x = cosf(theta) * sinf(phi) * radius;
|
|
||||||
float y = sinf(theta) * sinf(phi) * radius;
|
|
||||||
float z = cosf(phi) * radius;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ventajas:**
|
|
||||||
- Distribución uniforme sin clustering en polos
|
|
||||||
- O(1) cálculo por punto (no requiere iteraciones)
|
|
||||||
- Visualmente perfecto para demoscene effects
|
|
||||||
|
|
||||||
### Rotación 3D (Matrices de Rotación)
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// Rotación en eje Y (horizontal)
|
|
||||||
float cos_y = cosf(angle_y);
|
|
||||||
float sin_y = sinf(angle_y);
|
|
||||||
float x_rot = x * cos_y - z * sin_y;
|
|
||||||
float z_rot = x * sin_y + z * cos_y;
|
|
||||||
|
|
||||||
// Rotación en eje X (vertical)
|
|
||||||
float cos_x = cosf(angle_x);
|
|
||||||
float sin_x = sinf(angle_x);
|
|
||||||
float y_rot = y * cos_x - z_rot * sin_x;
|
|
||||||
float z_final = y * sin_x + z_rot * cos_x;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Velocidades:**
|
|
||||||
- Eje Y: 1.5 rad/s (rotación principal horizontal)
|
|
||||||
- Eje X: 0.8 rad/s (rotación secundaria vertical)
|
|
||||||
- Ratio Y/X ≈ 2:1 para efecto visual dinámico
|
|
||||||
|
|
||||||
### Proyección 3D → 2D
|
|
||||||
|
|
||||||
**Proyección Ortográfica:**
|
|
||||||
```cpp
|
|
||||||
float screen_x = center_x + x_rotated;
|
|
||||||
float screen_y = center_y + y_rotated;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Profundidad Z (Color Modulation):**
|
|
||||||
```cpp
|
|
||||||
// Normalizar Z de [-radius, +radius] a [0, 1]
|
|
||||||
float z_normalized = (z_final + radius) / (2.0f * radius);
|
|
||||||
|
|
||||||
// Mapear a rango de brillo [MIN_BRIGHTNESS, MAX_BRIGHTNESS]
|
|
||||||
float brightness_factor = (MIN + z_normalized * (MAX - MIN)) / 255.0f;
|
|
||||||
|
|
||||||
// Aplicar a color RGB
|
|
||||||
int r_mod = color.r * brightness_factor;
|
|
||||||
int g_mod = color.g * brightness_factor;
|
|
||||||
int b_mod = color.b * brightness_factor;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Efecto visual:**
|
|
||||||
- Z cerca (+radius): Brillo máximo (255) → Color original
|
|
||||||
- Z lejos (-radius): Brillo mínimo (50) → Color oscuro
|
|
||||||
- Simula profundidad sin sprites adicionales
|
|
||||||
|
|
||||||
### Transición Suave (Interpolación)
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// Progress de 0.0 a 1.0 en ROTOBALL_TRANSITION_TIME (1.5s)
|
|
||||||
transition_progress += delta_time / ROTOBALL_TRANSITION_TIME;
|
|
||||||
|
|
||||||
// Lerp desde posición actual a posición de esfera
|
|
||||||
float lerp_x = current_x + (target_sphere_x - current_x) * progress;
|
|
||||||
float lerp_y = current_y + (target_sphere_y - current_y) * progress;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- Independiente del framerate (usa delta_time)
|
|
||||||
- Suave y orgánico
|
|
||||||
- Sin pop visual
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
|
|
||||||
- **Batch rendering**: Una sola llamada `SDL_RenderGeometry` para todos los puntos
|
|
||||||
- **Recalculación**: Fibonacci sphere recalculada cada frame (O(n) predecible)
|
|
||||||
- **Sin malloc**: Usa datos ya almacenados en Ball objects
|
|
||||||
- **Color mod**: CPU-side, sin overhead GPU adicional
|
|
||||||
|
|
||||||
**Rendimiento medido:**
|
|
||||||
- 100 pelotas: >300 FPS
|
|
||||||
- 1,000 pelotas: >200 FPS
|
|
||||||
- 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 (VALORES ACTUALES - Amortiguamiento crítico)
|
|
||||||
ROTOBALL_SPRING_K = 300.0f; // Rigidez resorte
|
|
||||||
ROTOBALL_DAMPING_BASE = 35.0f; // Amortiguación lejos (crítico ≈ 2*√k*m)
|
|
||||||
ROTOBALL_DAMPING_NEAR = 80.0f; // Amortiguación cerca (absorción rápida)
|
|
||||||
ROTOBALL_NEAR_THRESHOLD = 5.0f; // Distancia "cerca" (px)
|
|
||||||
ROTOBALL_MAX_FORCE = 1000.0f; // Límite fuerza (seguridad)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Changelog de Ajustes:**
|
|
||||||
- **v1:** `DAMPING_BASE=15.0, NEAR=50.0` → Oscilación visible (subdamped)
|
|
||||||
- **v2:** `DAMPING_BASE=35.0, NEAR=80.0` → **Absorción rápida sin oscilación** ✅
|
|
||||||
|
|
||||||
### Ajustes Recomendados
|
|
||||||
|
|
||||||
**Si siguen oscilando (poco probable):**
|
|
||||||
```cpp
|
|
||||||
ROTOBALL_DAMPING_BASE = 50.0f; // Amortiguamiento super crítico
|
|
||||||
ROTOBALL_DAMPING_NEAR = 100.0f; // Absorción instantánea
|
|
||||||
```
|
|
||||||
|
|
||||||
**Si llegan muy lento:**
|
|
||||||
```cpp
|
|
||||||
ROTOBALL_SPRING_K = 400.0f; // Más fuerza
|
|
||||||
ROTOBALL_DAMPING_BASE = 40.0f; // Compensar con más damping
|
|
||||||
```
|
|
||||||
|
|
||||||
**Si quieres más "rebote" visual:**
|
|
||||||
```cpp
|
|
||||||
ROTOBALL_DAMPING_BASE = 25.0f; // Menos amortiguación
|
|
||||||
ROTOBALL_DAMPING_NEAR = 60.0f; // Ligera oscilación
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 Z-Sorting (Painter's Algorithm)
|
|
||||||
|
|
||||||
### Problema de Renderizado 3D
|
|
||||||
|
|
||||||
**Antes del Z-sorting:**
|
|
||||||
- Pelotas renderizadas en orden fijo del vector: `Ball[0] → Ball[1] → ... → Ball[N]`
|
|
||||||
- Orden aleatorio respecto a profundidad Z
|
|
||||||
- **Problema:** Pelotas oscuras (fondo) pintadas sobre claras (frente)
|
|
||||||
- Resultado: Inversión de profundidad visual incorrecta
|
|
||||||
|
|
||||||
**Después del Z-sorting:**
|
|
||||||
- Pelotas ordenadas por `depth_brightness` antes de renderizar
|
|
||||||
- Painter's Algorithm: **Fondo primero, frente último**
|
|
||||||
- Pelotas oscuras (Z bajo) renderizadas primero
|
|
||||||
- Pelotas claras (Z alto) renderizadas último (encima)
|
|
||||||
- **Resultado:** Oclusión 3D correcta ✅
|
|
||||||
|
|
||||||
### Implementación (engine.cpp::render())
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
if (current_mode_ == SimulationMode::ROTOBALL) {
|
|
||||||
// 1. Crear vector de índices
|
|
||||||
std::vector<size_t> render_order;
|
|
||||||
for (size_t i = 0; i < balls_.size(); i++) {
|
|
||||||
render_order.push_back(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Ordenar por depth_brightness (menor primero = fondo primero)
|
|
||||||
std::sort(render_order.begin(), render_order.end(),
|
|
||||||
[this](size_t a, size_t b) {
|
|
||||||
return balls_[a]->getDepthBrightness() < balls_[b]->getDepthBrightness();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Renderizar en orden de profundidad
|
|
||||||
for (size_t idx : render_order) {
|
|
||||||
// Renderizar balls_[idx]...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Complejidad y Performance
|
|
||||||
|
|
||||||
| Operación | Complejidad | Tiempo (estimado) |
|
|
||||||
|-----------|-------------|-------------------|
|
|
||||||
| Crear índices | O(n) | ~0.001ms (1K pelotas) |
|
|
||||||
| std::sort | O(n log n) | ~0.01ms (1K pelotas) |
|
|
||||||
| Renderizar | O(n) | ~variable |
|
|
||||||
| **Total** | **O(n log n)** | **~0.15ms (10K pelotas)** |
|
|
||||||
|
|
||||||
**Impacto en FPS:**
|
|
||||||
- 100 pelotas: Imperceptible (<0.001ms)
|
|
||||||
- 1,000 pelotas: Imperceptible (~0.01ms)
|
|
||||||
- 10,000 pelotas: Leve (~0.15ms, ~1-2 FPS)
|
|
||||||
- 100,000 pelotas: Moderado (~2ms, ~10-15 FPS)
|
|
||||||
|
|
||||||
### Optimizaciones Aplicadas
|
|
||||||
|
|
||||||
✅ **Solo en modo RotoBall**: Modo física no tiene overhead
|
|
||||||
✅ **Vector de índices**: `balls_` no se modifica (física estable)
|
|
||||||
✅ **Reserve() usado**: Evita realocaciones
|
|
||||||
✅ **Lambda eficiente**: Acceso directo sin copias
|
|
||||||
|
|
||||||
### Resultado Visual
|
|
||||||
|
|
||||||
✅ **Profundidad correcta**: Fondo detrás, frente delante
|
|
||||||
✅ **Oclusión apropiada**: Pelotas claras cubren oscuras
|
|
||||||
✅ **Efecto 3D realista**: Percepción de profundidad correcta
|
|
||||||
✅ **Sin artefactos visuales**: Ordenamiento estable cada frame
|
|
||||||
|
|
||||||
## Métricas del Proyecto
|
|
||||||
|
|
||||||
### ✅ Logros Actuales
|
|
||||||
- **Compilación exitosa** con CMake
|
|
||||||
- **Commit inicial** creado (dec8d43)
|
|
||||||
- **17 archivos** versionados
|
|
||||||
- **9,767 líneas** de código
|
|
||||||
- **Física direccional** 100% funcional
|
|
||||||
- **Coeficientes variables** implementados
|
|
||||||
|
|
||||||
### 🎯 Objetivos Cumplidos
|
|
||||||
- ✅ Migración limpia desde vibe1_delta
|
|
||||||
- ✅ Sistema de gravedad direccional implementado
|
|
||||||
- ✅ Coeficientes de rebote variables (+120% diversidad)
|
|
||||||
- ✅ **Modo RotoBall (esfera 3D rotante) implementado**
|
|
||||||
- ✅ **Fibonacci sphere algorithm funcionando**
|
|
||||||
- ✅ **Profundidad Z con color modulation**
|
|
||||||
- ✅ Debug display completo y funcional
|
|
||||||
- ✅ Controles intuitivos con teclas de cursor
|
|
||||||
- ✅ Eliminación de sincronización entre pelotas
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Comandos Útiles
|
|
||||||
|
|
||||||
### Compilación
|
|
||||||
```bash
|
|
||||||
mkdir -p build && cd build && cmake .. && cmake --build .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ejecución
|
|
||||||
```bash
|
|
||||||
./vibe3_physics.exe # Windows
|
|
||||||
./vibe3_physics # Linux/macOS
|
|
||||||
```
|
|
||||||
|
|
||||||
### Git
|
|
||||||
```bash
|
|
||||||
git status # Ver cambios
|
|
||||||
git add . # Añadir archivos
|
|
||||||
git commit -m "..." # Crear commit
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Archivo de seguimiento para sesiones Claude Code - ViBe3 Physics*
|
|
||||||
*Actualizado: Implementación de gravedad direccional completada*
|
|
||||||
@@ -11,15 +11,59 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Os -ffunction-sections -fdata-sec
|
|||||||
# Buscar SDL3 automáticamente
|
# Buscar SDL3 automáticamente
|
||||||
find_package(SDL3 REQUIRED)
|
find_package(SDL3 REQUIRED)
|
||||||
|
|
||||||
# Si no se encuentra SDL3, generar un error
|
# Buscar SDL3_ttf
|
||||||
if (NOT SDL3_FOUND)
|
find_package(SDL3_ttf REQUIRED)
|
||||||
message(FATAL_ERROR "SDL3 no encontrado. Por favor, verifica su instalación.")
|
|
||||||
|
# ---- Shader compilation (non-Apple only: Vulkan/SPIRV) ----
|
||||||
|
if(NOT APPLE)
|
||||||
|
find_program(GLSLC glslc HINTS "$ENV{VULKAN_SDK}/bin" "$ENV{VULKAN_SDK}/Bin")
|
||||||
|
if(NOT GLSLC)
|
||||||
|
message(STATUS "glslc not found — using precompiled SPIR-V headers from shaders/precompiled/")
|
||||||
|
set(SHADER_GEN_DIR "${CMAKE_SOURCE_DIR}/shaders/precompiled")
|
||||||
|
else()
|
||||||
|
set(SHADER_SRC_DIR "${CMAKE_SOURCE_DIR}/shaders")
|
||||||
|
set(SHADER_GEN_DIR "${CMAKE_BINARY_DIR}/generated_shaders")
|
||||||
|
file(MAKE_DIRECTORY "${SHADER_GEN_DIR}")
|
||||||
|
|
||||||
|
set(SPIRV_HEADERS)
|
||||||
|
foreach(SHADER sprite_vert sprite_frag postfx_vert postfx_frag ball_vert)
|
||||||
|
if(SHADER MATCHES "_vert$")
|
||||||
|
set(STAGE_FLAG "-fshader-stage=vertex")
|
||||||
|
else()
|
||||||
|
set(STAGE_FLAG "-fshader-stage=fragment")
|
||||||
|
endif()
|
||||||
|
string(REGEX REPLACE "_vert$" ".vert" GLSL_NAME "${SHADER}")
|
||||||
|
string(REGEX REPLACE "_frag$" ".frag" GLSL_NAME "${GLSL_NAME}")
|
||||||
|
|
||||||
|
set(GLSL_FILE "${SHADER_SRC_DIR}/${GLSL_NAME}")
|
||||||
|
set(SPV_FILE "${SHADER_GEN_DIR}/${SHADER}.spv")
|
||||||
|
set(H_FILE "${SHADER_GEN_DIR}/${SHADER}_spv.h")
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${H_FILE}"
|
||||||
|
COMMAND "${GLSLC}" ${STAGE_FLAG} -o "${SPV_FILE}" "${GLSL_FILE}"
|
||||||
|
COMMAND "${CMAKE_COMMAND}"
|
||||||
|
-DINPUT="${SPV_FILE}"
|
||||||
|
-DOUTPUT="${H_FILE}"
|
||||||
|
-DVAR_NAME="k${SHADER}_spv"
|
||||||
|
-P "${CMAKE_SOURCE_DIR}/cmake/spv_to_header.cmake"
|
||||||
|
DEPENDS "${GLSL_FILE}"
|
||||||
|
COMMENT "Compiling ${GLSL_NAME} to SPIRV"
|
||||||
|
)
|
||||||
|
list(APPEND SPIRV_HEADERS "${H_FILE}")
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
add_custom_target(shaders ALL DEPENDS ${SPIRV_HEADERS})
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Archivos fuente (excluir main_old.cpp)
|
# Archivos fuente (excluir main_old.cpp)
|
||||||
file(GLOB SOURCE_FILES source/*.cpp source/external/*.cpp)
|
file(GLOB SOURCE_FILES source/*.cpp source/external/*.cpp source/boids_mgr/*.cpp source/gpu/*.cpp source/input/*.cpp source/scene/*.cpp source/shapes/*.cpp source/shapes_mgr/*.cpp source/state/*.cpp source/themes/*.cpp source/text/*.cpp source/ui/*.cpp)
|
||||||
list(REMOVE_ITEM SOURCE_FILES "${CMAKE_SOURCE_DIR}/source/main_old.cpp")
|
list(REMOVE_ITEM SOURCE_FILES "${CMAKE_SOURCE_DIR}/source/main_old.cpp")
|
||||||
|
|
||||||
|
# Suprimir falso positivo de GCC en stb_image.h (externo)
|
||||||
|
set_source_files_properties(source/external/texture.cpp PROPERTIES COMPILE_FLAGS "-Wno-stringop-overflow")
|
||||||
|
|
||||||
# Comprobar si se encontraron archivos fuente
|
# Comprobar si se encontraron archivos fuente
|
||||||
if(NOT SOURCE_FILES)
|
if(NOT SOURCE_FILES)
|
||||||
message(FATAL_ERROR "No se encontraron archivos fuente en el directorio 'source/'. Verifica la ruta.")
|
message(FATAL_ERROR "No se encontraron archivos fuente en el directorio 'source/'. Verifica la ruta.")
|
||||||
@@ -27,18 +71,18 @@ endif()
|
|||||||
|
|
||||||
# Detectar la plataforma y configuraciones específicas
|
# Detectar la plataforma y configuraciones específicas
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
set(PLATFORM windows)
|
set(LINK_LIBS SDL3::SDL3 SDL3_ttf::SDL3_ttf mingw32 ws2_32)
|
||||||
set(LINK_LIBS ${SDL3_LIBRARIES} mingw32 ws2_32)
|
|
||||||
elseif(UNIX AND NOT APPLE)
|
elseif(UNIX AND NOT APPLE)
|
||||||
set(PLATFORM linux)
|
set(LINK_LIBS SDL3::SDL3 SDL3_ttf::SDL3_ttf)
|
||||||
set(LINK_LIBS ${SDL3_LIBRARIES})
|
|
||||||
elseif(APPLE)
|
elseif(APPLE)
|
||||||
set(PLATFORM macos)
|
set(LINK_LIBS SDL3::SDL3 SDL3_ttf::SDL3_ttf)
|
||||||
set(LINK_LIBS ${SDL3_LIBRARIES})
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Incluir directorios de SDL3
|
# Incluir directorios de SDL3 y SDL3_ttf
|
||||||
include_directories(${SDL3_INCLUDE_DIRS})
|
include_directories(${SDL3_INCLUDE_DIRS} ${SDL3_ttf_INCLUDE_DIRS})
|
||||||
|
|
||||||
|
# Incluir directorio source/ para poder usar includes desde la raíz del proyecto
|
||||||
|
include_directories(${CMAKE_SOURCE_DIR}/source)
|
||||||
|
|
||||||
# Añadir el ejecutable reutilizando el nombre del proyecto
|
# Añadir el ejecutable reutilizando el nombre del proyecto
|
||||||
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
|
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
|
||||||
@@ -48,3 +92,15 @@ set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAK
|
|||||||
|
|
||||||
# Enlazar las bibliotecas necesarias
|
# Enlazar las bibliotecas necesarias
|
||||||
target_link_libraries(${PROJECT_NAME} ${LINK_LIBS})
|
target_link_libraries(${PROJECT_NAME} ${LINK_LIBS})
|
||||||
|
|
||||||
|
if(NOT APPLE)
|
||||||
|
if(GLSLC)
|
||||||
|
add_dependencies(${PROJECT_NAME} shaders)
|
||||||
|
endif()
|
||||||
|
target_include_directories(${PROJECT_NAME} PRIVATE "${SHADER_GEN_DIR}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Tool: pack_resources
|
||||||
|
add_executable(pack_resources tools/pack_resources.cpp source/resource_pack.cpp)
|
||||||
|
target_include_directories(pack_resources PRIVATE ${CMAKE_SOURCE_DIR}/source)
|
||||||
|
set_target_properties(pack_resources PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tools")
|
||||||
|
|||||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License
|
||||||
|
|
||||||
|
Copyright (c) 2025-2026 ViBe3 Physics - JailDesigner
|
||||||
|
|
||||||
|
This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||||
|
|
||||||
|
You are free to:
|
||||||
|
- Share — copy and redistribute the material in any medium or format
|
||||||
|
- Adapt — remix, transform, and build upon the material
|
||||||
|
|
||||||
|
Under the following terms:
|
||||||
|
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
|
||||||
|
- NonCommercial — You may not use the material for commercial purposes.
|
||||||
|
- ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
|
||||||
|
|
||||||
|
No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
|
||||||
|
|
||||||
|
To view a copy of this license, visit:
|
||||||
|
https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
424
Makefile
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
# Directorios
|
||||||
|
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
|
||||||
|
DIR_SOURCES := $(addsuffix /, $(DIR_ROOT)source)
|
||||||
|
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
|
||||||
|
DIR_TOOLS := $(addsuffix /, $(DIR_ROOT)tools)
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
TARGET_NAME := vibe3_physics
|
||||||
|
TARGET_FILE := $(DIR_BIN)$(TARGET_NAME)
|
||||||
|
APP_NAME := ViBe3 Physics
|
||||||
|
RELEASE_FOLDER := dist/_tmp
|
||||||
|
RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
|
||||||
|
RESOURCE_FILE := build/vibe3.res
|
||||||
|
DIST_DIR := dist
|
||||||
|
|
||||||
|
# Variables para herramienta de empaquetado
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
PACK_TOOL := $(DIR_TOOLS)pack_resources.exe
|
||||||
|
PACK_CXX := $(CXX)
|
||||||
|
else
|
||||||
|
PACK_TOOL := $(DIR_TOOLS)pack_resources
|
||||||
|
PACK_CXX := $(CXX)
|
||||||
|
endif
|
||||||
|
PACK_SOURCES := $(DIR_TOOLS)pack_resources.cpp $(DIR_SOURCES)resource_pack.cpp
|
||||||
|
PACK_INCLUDES := -I$(DIR_ROOT)
|
||||||
|
|
||||||
|
# Versión automática basada en la fecha actual (específica por SO)
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
VERSION := $(shell powershell -Command "Get-Date -Format 'yyyy-MM-dd'")
|
||||||
|
else
|
||||||
|
VERSION := $(shell date +%Y-%m-%d)
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Variables específicas para Windows (usando APP_NAME)
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
WIN_TARGET_FILE := $(DIR_BIN)$(APP_NAME)
|
||||||
|
WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(APP_NAME)
|
||||||
|
else
|
||||||
|
WIN_TARGET_FILE := $(TARGET_FILE)
|
||||||
|
WIN_RELEASE_FILE := $(RELEASE_FILE)
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Nombres para los ficheros de lanzamiento
|
||||||
|
WINDOWS_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-win32-x64.zip
|
||||||
|
MACOS_INTEL_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-macos-intel.dmg
|
||||||
|
MACOS_APPLE_SILICON_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-macos-apple-silicon.dmg
|
||||||
|
LINUX_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-linux.tar.gz
|
||||||
|
RASPI_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-raspberry.tar.gz
|
||||||
|
|
||||||
|
# Lista completa de archivos fuente (detección automática con wildcards, como CMakeLists.txt)
|
||||||
|
APP_SOURCES := $(wildcard source/*.cpp) \
|
||||||
|
$(wildcard source/external/*.cpp) \
|
||||||
|
$(wildcard source/gpu/*.cpp) \
|
||||||
|
$(wildcard source/shapes/*.cpp) \
|
||||||
|
$(wildcard source/themes/*.cpp) \
|
||||||
|
$(wildcard source/state/*.cpp) \
|
||||||
|
$(wildcard source/input/*.cpp) \
|
||||||
|
$(wildcard source/scene/*.cpp) \
|
||||||
|
$(wildcard source/shapes_mgr/*.cpp) \
|
||||||
|
$(wildcard source/boids_mgr/*.cpp) \
|
||||||
|
$(wildcard source/text/*.cpp) \
|
||||||
|
$(wildcard source/ui/*.cpp)
|
||||||
|
|
||||||
|
# Excluir archivos antiguos si existen
|
||||||
|
APP_SOURCES := $(filter-out source/main_old.cpp, $(APP_SOURCES))
|
||||||
|
|
||||||
|
# Includes: usar shaders pre-compilados si glslc no está disponible
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
GLSLC := $(shell where glslc 2>NUL)
|
||||||
|
else
|
||||||
|
GLSLC := $(shell command -v glslc 2>/dev/null)
|
||||||
|
endif
|
||||||
|
ifeq ($(GLSLC),)
|
||||||
|
SHADER_INCLUDE := -Ishaders/precompiled
|
||||||
|
else
|
||||||
|
SHADER_INCLUDE := -Ibuild/generated_shaders
|
||||||
|
endif
|
||||||
|
INCLUDES := -Isource -Isource/external $(SHADER_INCLUDE)
|
||||||
|
|
||||||
|
# Variables según el sistema operativo
|
||||||
|
CXXFLAGS_BASE := -std=c++20 -Wall
|
||||||
|
CXXFLAGS := $(CXXFLAGS_BASE) -Os -ffunction-sections -fdata-sections
|
||||||
|
LDFLAGS :=
|
||||||
|
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
FixPath = $(subst /,\\,$1)
|
||||||
|
CXXFLAGS += -DWINDOWS_BUILD
|
||||||
|
LDFLAGS += -Wl,--gc-sections -static-libstdc++ -static-libgcc \
|
||||||
|
-Wl,-Bstatic -lpthread -Wl,-Bdynamic -Wl,-subsystem,windows \
|
||||||
|
-lmingw32 -lws2_32 -lSDL3 -lSDL3_ttf
|
||||||
|
RMFILE := del /Q
|
||||||
|
RMDIR := rmdir /S /Q
|
||||||
|
MKDIR := mkdir
|
||||||
|
else
|
||||||
|
FixPath = $1
|
||||||
|
LDFLAGS += -lSDL3 -lSDL3_ttf
|
||||||
|
RMFILE := rm -f
|
||||||
|
RMDIR := rm -rf
|
||||||
|
MKDIR := mkdir -p
|
||||||
|
UNAME_S := $(shell uname -s)
|
||||||
|
ifeq ($(UNAME_S),Linux)
|
||||||
|
CXXFLAGS += -DLINUX_BUILD
|
||||||
|
endif
|
||||||
|
ifeq ($(UNAME_S),Darwin)
|
||||||
|
CXXFLAGS += -DMACOS_BUILD -arch arm64
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
|
# Reglas para herramienta de empaquetado y resources.pack
|
||||||
|
$(PACK_TOOL): $(PACK_SOURCES)
|
||||||
|
@echo "Compilando herramienta de empaquetado..."
|
||||||
|
$(PACK_CXX) -std=c++20 -Wall -Os $(PACK_INCLUDES) $(PACK_SOURCES) -o $(PACK_TOOL)
|
||||||
|
@echo "✓ Herramienta de empaquetado lista: $(PACK_TOOL)"
|
||||||
|
|
||||||
|
pack_tool: $(PACK_TOOL)
|
||||||
|
|
||||||
|
# Detectar todos los archivos en data/ como dependencias (regenera si cualquiera cambia)
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
DATA_FILES :=
|
||||||
|
else
|
||||||
|
DATA_FILES := $(shell find data -type f 2>/dev/null)
|
||||||
|
endif
|
||||||
|
|
||||||
|
resources.pack: $(PACK_TOOL) $(DATA_FILES)
|
||||||
|
@echo "Generando resources.pack desde directorio data/..."
|
||||||
|
$(PACK_TOOL) data resources.pack
|
||||||
|
@echo "✓ resources.pack generado exitosamente"
|
||||||
|
|
||||||
|
# Target para forzar regeneración de resources.pack (usado por releases)
|
||||||
|
.PHONY: force_resource_pack
|
||||||
|
force_resource_pack: $(PACK_TOOL)
|
||||||
|
@echo "Regenerando resources.pack para release..."
|
||||||
|
$(PACK_TOOL) data resources.pack
|
||||||
|
@echo "✓ resources.pack regenerado exitosamente"
|
||||||
|
|
||||||
|
# Reglas para compilación
|
||||||
|
windows:
|
||||||
|
@echo Compilando para Windows con nombre: $(APP_NAME).exe
|
||||||
|
windres release/windows/vibe3.rc -O coff -o $(RESOURCE_FILE)
|
||||||
|
$(CXX) $(APP_SOURCES) $(RESOURCE_FILE) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(WIN_TARGET_FILE).exe"
|
||||||
|
strip -s -R .comment -R .gnu.version "$(WIN_TARGET_FILE).exe" --strip-unneeded
|
||||||
|
|
||||||
|
windows_release: force_resource_pack
|
||||||
|
@echo "Creando release para Windows - Version: $(VERSION)"
|
||||||
|
|
||||||
|
# Crea carpeta temporal 'RELEASE_FOLDER'
|
||||||
|
@if exist "$(RELEASE_FOLDER)" rmdir /S /Q "$(RELEASE_FOLDER)"
|
||||||
|
@if not exist "$(DIST_DIR)" mkdir "$(DIST_DIR)"
|
||||||
|
@mkdir "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
|
# Copia el archivo 'resources.pack'
|
||||||
|
@copy /Y "resources.pack" "$(RELEASE_FOLDER)\" >nul
|
||||||
|
|
||||||
|
# Copia los ficheros que estan en la raíz del proyecto
|
||||||
|
@copy /Y "LICENSE" "$(RELEASE_FOLDER)\" >nul 2>&1 || echo LICENSE not found (optional)
|
||||||
|
@copy /Y "README.md" "$(RELEASE_FOLDER)\" >nul
|
||||||
|
@copy /Y release\windows\dll\*.dll "$(RELEASE_FOLDER)\" >nul 2>&1 || echo DLLs copied successfully
|
||||||
|
|
||||||
|
# Compila
|
||||||
|
@windres release/windows/vibe3.rc -O coff -o $(RESOURCE_FILE)
|
||||||
|
@$(CXX) $(APP_SOURCES) $(RESOURCE_FILE) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(WIN_RELEASE_FILE).exe"
|
||||||
|
@strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
|
||||||
|
|
||||||
|
# Crea el fichero .zip
|
||||||
|
@if exist "$(WINDOWS_RELEASE)" del /Q "$(WINDOWS_RELEASE)"
|
||||||
|
@powershell.exe -Command "Compress-Archive -Path '$(RELEASE_FOLDER)/*' -DestinationPath '$(WINDOWS_RELEASE)' -Force"
|
||||||
|
@echo "Release creado: $(WINDOWS_RELEASE)"
|
||||||
|
|
||||||
|
# Elimina la carpeta temporal 'RELEASE_FOLDER'
|
||||||
|
@rmdir /S /Q "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
|
macos:
|
||||||
|
@echo "Compilando para macOS: $(TARGET_NAME)"
|
||||||
|
$(CXX) $(APP_SOURCES) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(TARGET_FILE)"
|
||||||
|
|
||||||
|
macos_release: force_resource_pack
|
||||||
|
@echo "Creando release para macOS - Version: $(VERSION)"
|
||||||
|
|
||||||
|
# Verificar e instalar create-dmg si es necesario
|
||||||
|
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg)
|
||||||
|
|
||||||
|
# Elimina datos de compilaciones anteriores
|
||||||
|
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
|
$(RMDIR) Frameworks
|
||||||
|
$(RMFILE) "$(MACOS_INTEL_RELEASE)"
|
||||||
|
$(RMFILE) "$(MACOS_APPLE_SILICON_RELEASE)"
|
||||||
|
|
||||||
|
# Limpia archivos temporales de create-dmg y desmonta volúmenes
|
||||||
|
@echo "Limpiando archivos temporales y volúmenes montados..."
|
||||||
|
@rm -f rw.*.dmg 2>/dev/null || true
|
||||||
|
@hdiutil detach "/Volumes/$(APP_NAME)" 2>/dev/null || true
|
||||||
|
@hdiutil detach "/Volumes/ViBe3 Physics" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Crea la carpeta temporal para hacer el trabajo y las carpetas obligatorias para crear una app de macos
|
||||||
|
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
|
||||||
|
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS"
|
||||||
|
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
|
$(MKDIR) Frameworks
|
||||||
|
|
||||||
|
# Copia carpetas y ficheros
|
||||||
|
cp resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
|
cp -R release/macos/frameworks/SDL3.xcframework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
|
||||||
|
cp -R release/macos/frameworks/SDL3_ttf.xcframework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
|
||||||
|
cp -R release/macos/frameworks/SDL3.xcframework Frameworks
|
||||||
|
cp -R release/macos/frameworks/SDL3_ttf.xcframework Frameworks
|
||||||
|
cp release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
|
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
|
||||||
|
$(MKDIR) "$(DIST_DIR)"
|
||||||
|
cp LICENSE "$(RELEASE_FOLDER)"
|
||||||
|
cp README.md "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
|
# Compila la versión para procesadores Apple Silicon
|
||||||
|
$(CXX) $(APP_SOURCES) $(INCLUDES) -DMACOS_BUNDLE -DSDL_DISABLE_IMMINTRIN_H $(CXXFLAGS) $(LDFLAGS) -o "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" -rpath @executable_path/../Frameworks/ -target arm64-apple-macos12
|
||||||
|
|
||||||
|
# Firma la aplicación
|
||||||
|
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
|
||||||
|
|
||||||
|
# Empaqueta el .dmg de la versión Apple Silicon con create-dmg
|
||||||
|
@echo "Creando DMG Apple Silicon con iconos de 96x96..."
|
||||||
|
@create-dmg \
|
||||||
|
--volname "$(APP_NAME)" \
|
||||||
|
--window-pos 200 120 \
|
||||||
|
--window-size 720 300 \
|
||||||
|
--icon-size 96 \
|
||||||
|
--text-size 12 \
|
||||||
|
--icon "$(APP_NAME).app" 278 102 \
|
||||||
|
--icon "LICENSE" 441 102 \
|
||||||
|
--icon "README.md" 604 102 \
|
||||||
|
--app-drop-link 115 102 \
|
||||||
|
--hide-extension "$(APP_NAME).app" \
|
||||||
|
"$(MACOS_APPLE_SILICON_RELEASE)" \
|
||||||
|
"$(RELEASE_FOLDER)"
|
||||||
|
@if [ -f "$(MACOS_APPLE_SILICON_RELEASE)" ]; then \
|
||||||
|
echo "✓ Release Apple Silicon creado exitosamente: $(MACOS_APPLE_SILICON_RELEASE)"; \
|
||||||
|
else \
|
||||||
|
echo "✗ Error: No se pudo crear el DMG Apple Silicon"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@rm -f rw.*.dmg 2>/dev/null || true
|
||||||
|
|
||||||
|
# Elimina las carpetas temporales
|
||||||
|
$(RMDIR) Frameworks
|
||||||
|
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
|
linux:
|
||||||
|
@echo "Compilando para Linux: $(TARGET_NAME)"
|
||||||
|
$(CXX) $(APP_SOURCES) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(TARGET_FILE)"
|
||||||
|
strip -s -R .comment -R .gnu.version "$(TARGET_FILE)" --strip-unneeded
|
||||||
|
|
||||||
|
linux_release: force_resource_pack
|
||||||
|
@echo "Creando release para Linux - Version: $(VERSION)"
|
||||||
|
# Elimina carpetas previas
|
||||||
|
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
|
# Crea la carpeta temporal para realizar el lanzamiento
|
||||||
|
$(MKDIR) "$(RELEASE_FOLDER)"
|
||||||
|
$(MKDIR) "$(DIST_DIR)"
|
||||||
|
|
||||||
|
# Copia ficheros
|
||||||
|
cp resources.pack "$(RELEASE_FOLDER)"
|
||||||
|
cp LICENSE "$(RELEASE_FOLDER)"
|
||||||
|
cp README.md "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
|
# Compila
|
||||||
|
$(CXX) $(APP_SOURCES) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(RELEASE_FILE)"
|
||||||
|
strip -s -R .comment -R .gnu.version "$(RELEASE_FILE)" --strip-unneeded
|
||||||
|
|
||||||
|
# Empaqueta ficheros
|
||||||
|
$(RMFILE) "$(LINUX_RELEASE)"
|
||||||
|
tar -czvf "$(LINUX_RELEASE)" -C "$(RELEASE_FOLDER)" .
|
||||||
|
@echo "Release creado: $(LINUX_RELEASE)"
|
||||||
|
|
||||||
|
# Elimina la carpeta temporal
|
||||||
|
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
|
linux_release_desktop: force_resource_pack
|
||||||
|
@echo "Creando release con integracion desktop para Linux - Version: $(VERSION)"
|
||||||
|
# Elimina carpetas previas
|
||||||
|
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
|
# Crea la carpeta de distribución y la estructura de directorios estándar para Linux
|
||||||
|
$(MKDIR) "$(DIST_DIR)"
|
||||||
|
$(MKDIR) "$(RELEASE_FOLDER)/$(TARGET_NAME)"
|
||||||
|
$(MKDIR) "$(RELEASE_FOLDER)/$(TARGET_NAME)/bin"
|
||||||
|
$(MKDIR) "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications"
|
||||||
|
$(MKDIR) "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/icons/hicolor/256x256/apps"
|
||||||
|
$(MKDIR) "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/$(TARGET_NAME)"
|
||||||
|
|
||||||
|
# Copia ficheros del juego
|
||||||
|
cp resources.pack "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/$(TARGET_NAME)/"
|
||||||
|
cp LICENSE "$(RELEASE_FOLDER)/$(TARGET_NAME)/"
|
||||||
|
cp README.md "$(RELEASE_FOLDER)/$(TARGET_NAME)/"
|
||||||
|
|
||||||
|
# Compila el ejecutable
|
||||||
|
$(CXX) $(APP_SOURCES) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(RELEASE_FOLDER)/$(TARGET_NAME)/bin/$(TARGET_NAME)"
|
||||||
|
strip -s -R .comment -R .gnu.version "$(RELEASE_FOLDER)/$(TARGET_NAME)/bin/$(TARGET_NAME)" --strip-unneeded
|
||||||
|
|
||||||
|
# Crea el archivo .desktop
|
||||||
|
@echo '[Desktop Entry]' > "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||||
|
@echo 'Version=1.0' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||||
|
@echo 'Type=Application' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||||
|
@echo 'Name=$(APP_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||||
|
@echo 'Comment=Arcade action game - defend Earth from alien invasion!' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||||
|
@echo 'Exec=/opt/$(TARGET_NAME)/bin/$(TARGET_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||||
|
@echo 'Icon=$(TARGET_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||||
|
@echo 'Path=/opt/$(TARGET_NAME)/share/$(TARGET_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||||
|
@echo 'Terminal=false' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||||
|
@echo 'StartupNotify=true' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||||
|
@echo 'Categories=Game;ArcadeGame;' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||||
|
@echo 'Keywords=arcade;action;shooter;retro;' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||||
|
|
||||||
|
# Copia el icono (si existe) y lo redimensiona si es necesario
|
||||||
|
@if [ -f "release/icons/icon.png" ]; then \
|
||||||
|
if command -v magick >/dev/null 2>&1; then \
|
||||||
|
magick "release/icons/icon.png" -resize 256x256 "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png"; \
|
||||||
|
echo "Icono redimensionado de release/icons/icon.png (usando ImageMagick)"; \
|
||||||
|
elif command -v convert >/dev/null 2>&1; then \
|
||||||
|
convert "release/icons/icon.png" -resize 256x256 "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png"; \
|
||||||
|
echo "Icono redimensionado de release/icons/icon.png (usando ImageMagick legacy)"; \
|
||||||
|
elif command -v ffmpeg >/dev/null 2>&1; then \
|
||||||
|
ffmpeg -i "release/icons/icon.png" -vf scale=256:256 "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png" -y -loglevel quiet; \
|
||||||
|
echo "Icono redimensionado de release/icons/icon.png (usando ffmpeg)"; \
|
||||||
|
else \
|
||||||
|
cp "release/icons/icon.png" "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png"; \
|
||||||
|
echo "Icono copiado sin redimensionar (instalar ImageMagick o ffmpeg para redimensionado automatico)"; \
|
||||||
|
fi; \
|
||||||
|
else \
|
||||||
|
echo "Advertencia: No se encontró release/icons/icon.png - crear icono manualmente"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Crea script de instalación
|
||||||
|
@echo '#!/bin/bash' > "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
@echo 'echo "Instalando $(APP_NAME)..."' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
@echo 'sudo mkdir -p /opt/$(TARGET_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
@echo 'sudo cp -R bin /opt/$(TARGET_NAME)/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
@echo 'sudo cp -R share /opt/$(TARGET_NAME)/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
@echo 'sudo cp LICENSE /opt/$(TARGET_NAME)/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
@echo 'sudo cp README.md /opt/$(TARGET_NAME)/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
@echo 'sudo mkdir -p /usr/share/applications' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
@echo 'sudo mkdir -p /usr/share/icons/hicolor/256x256/apps' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
@echo 'sudo cp /opt/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop /usr/share/applications/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
@echo 'sudo cp /opt/$(TARGET_NAME)/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png /usr/share/icons/hicolor/256x256/apps/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
@echo 'sudo update-desktop-database /usr/share/applications 2>/dev/null || true' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
@echo 'sudo gtk-update-icon-cache /usr/share/icons/hicolor 2>/dev/null || true' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
@echo 'echo "$(APP_NAME) instalado correctamente!"' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
@echo 'echo "Ya puedes encontrarlo en el menu de aplicaciones en la categoria Juegos."' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
chmod +x "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||||
|
|
||||||
|
# Crea script de desinstalación
|
||||||
|
@echo '#!/bin/bash' > "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||||
|
@echo 'echo "Desinstalando $(APP_NAME)..."' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||||
|
@echo 'sudo rm -rf /opt/$(TARGET_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||||
|
@echo 'sudo rm -f /usr/share/applications/$(TARGET_NAME).desktop' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||||
|
@echo 'sudo rm -f /usr/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||||
|
@echo 'sudo update-desktop-database /usr/share/applications 2>/dev/null || true' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||||
|
@echo 'sudo gtk-update-icon-cache /usr/share/icons/hicolor 2>/dev/null || true' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||||
|
@echo 'echo "$(APP_NAME) desinstalado correctamente."' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||||
|
chmod +x "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||||
|
|
||||||
|
# Empaqueta ficheros
|
||||||
|
$(RMFILE) "$(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-linux-desktop.tar.gz"
|
||||||
|
tar -czvf "$(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-linux-desktop.tar.gz" -C "$(RELEASE_FOLDER)" .
|
||||||
|
@echo "Release con integracion desktop creado: $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-linux-desktop.tar.gz"
|
||||||
|
@echo "Para instalar: extraer y ejecutar ./$(TARGET_NAME)/install.sh"
|
||||||
|
|
||||||
|
# Elimina la carpeta temporal
|
||||||
|
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
|
raspi:
|
||||||
|
@echo "Compilando para Raspberry Pi: $(TARGET_NAME)"
|
||||||
|
$(CXX) $(APP_SOURCES) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o $(TARGET_FILE)
|
||||||
|
strip -s -R .comment -R .gnu.version $(TARGET_FILE) --strip-unneeded
|
||||||
|
|
||||||
|
raspi_release: force_resource_pack
|
||||||
|
@echo "Creando release para Raspberry Pi - Version: $(VERSION)"
|
||||||
|
# Elimina carpetas previas
|
||||||
|
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
|
# Crea la carpeta temporal para realizar el lanzamiento
|
||||||
|
$(MKDIR) "$(RELEASE_FOLDER)"
|
||||||
|
$(MKDIR) "$(DIST_DIR)"
|
||||||
|
|
||||||
|
# Copia ficheros
|
||||||
|
cp resources.pack "$(RELEASE_FOLDER)"
|
||||||
|
cp LICENSE "$(RELEASE_FOLDER)"
|
||||||
|
cp README.md "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
|
# Compila
|
||||||
|
$(CXX) $(APP_SOURCES) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(RELEASE_FILE)"
|
||||||
|
strip -s -R .comment -R .gnu.version "$(RELEASE_FILE)" --strip-unneeded
|
||||||
|
|
||||||
|
# Empaqueta ficheros
|
||||||
|
$(RMFILE) "$(RASPI_RELEASE)"
|
||||||
|
tar -czvf "$(RASPI_RELEASE)" -C "$(RELEASE_FOLDER)" .
|
||||||
|
@echo "Release creado: $(RASPI_RELEASE)"
|
||||||
|
|
||||||
|
# Elimina la carpeta temporal
|
||||||
|
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
|
# Regla para mostrar la versión actual
|
||||||
|
show_version:
|
||||||
|
@echo "Version actual: $(VERSION)"
|
||||||
|
|
||||||
|
# Regla de ayuda
|
||||||
|
help:
|
||||||
|
@echo "Makefile para ViBe3 Physics"
|
||||||
|
@echo "Comandos disponibles:"
|
||||||
|
@echo " windows - Compilar para Windows"
|
||||||
|
@echo " windows_release - Crear release completo para Windows (.zip)"
|
||||||
|
@echo " linux - Compilar para Linux"
|
||||||
|
@echo " linux_release - Crear release basico para Linux (.tar.gz)"
|
||||||
|
@echo " linux_release_desktop - Crear release con integracion desktop para Linux"
|
||||||
|
@echo " macos - Compilar para macOS"
|
||||||
|
@echo " macos_release - Crear release completo para macOS (.dmg)"
|
||||||
|
@echo " raspi - Compilar para Raspberry Pi"
|
||||||
|
@echo " raspi_release - Crear release para Raspberry Pi (.tar.gz)"
|
||||||
|
@echo " pack_tool - Compilar herramienta de empaquetado"
|
||||||
|
@echo " resources.pack - Generar pack de recursos desde data/"
|
||||||
|
@echo " force_resource_pack - Regenerar resources.pack (usado por releases)"
|
||||||
|
@echo " show_version - Mostrar version actual ($(VERSION))"
|
||||||
|
@echo " help - Mostrar esta ayuda"
|
||||||
|
|
||||||
|
.PHONY: windows windows_release macos macos_release linux linux_release linux_release_desktop raspi raspi_release show_version help pack_tool force_resource_pack
|
||||||
20
cmake/spv_to_header.cmake
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Converts a SPIR-V binary to a C++ header with an embedded uint8_t array.
|
||||||
|
# cmake -DINPUT=<spv> -DOUTPUT=<h> -DVAR_NAME=<name> -P spv_to_header.cmake
|
||||||
|
|
||||||
|
if(NOT DEFINED INPUT OR NOT DEFINED OUTPUT OR NOT DEFINED VAR_NAME)
|
||||||
|
message(FATAL_ERROR "Usage: -DINPUT=x.spv -DOUTPUT=x.h -DVAR_NAME=kname -P spv_to_header.cmake")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
file(READ "${INPUT}" raw_hex HEX)
|
||||||
|
string(REGEX REPLACE "([0-9a-fA-F][0-9a-fA-F])" "0x\\1," hex_bytes "${raw_hex}")
|
||||||
|
string(REGEX REPLACE ",$" "" hex_bytes "${hex_bytes}")
|
||||||
|
string(LENGTH "${raw_hex}" hex_len)
|
||||||
|
math(EXPR byte_count "${hex_len} / 2")
|
||||||
|
|
||||||
|
file(WRITE "${OUTPUT}"
|
||||||
|
"#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstddef>
|
||||||
|
static const uint8_t ${VAR_NAME}[] = { ${hex_bytes} };
|
||||||
|
static const size_t ${VAR_NAME}_size = ${byte_count};
|
||||||
|
")
|
||||||
BIN
data/ball.png
|
Before Width: | Height: | Size: 162 B |
BIN
data/balls/big.png
Normal file
|
After Width: | Height: | Size: 177 B |
BIN
data/balls/normal.png
Normal file
|
After Width: | Height: | Size: 169 B |
BIN
data/balls/small.png
Normal file
|
After Width: | Height: | Size: 122 B |
BIN
data/balls/tiny.png
Normal file
|
After Width: | Height: | Size: 99 B |
BIN
data/fonts/Exo2-Regular.ttf
Normal file
BIN
data/logo/logo.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
data/logo/logo2.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
data/shapes/jailgames.png
Normal file
|
After Width: | Height: | Size: 618 B |
150
release/icons/create_icons.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def check_dependencies():
|
||||||
|
"""Verifica que ImageMagick esté instalado"""
|
||||||
|
try:
|
||||||
|
subprocess.run(['magick', '--version'], capture_output=True, check=True)
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
print("Error: ImageMagick no está instalado o no se encuentra en el PATH")
|
||||||
|
print("Instala ImageMagick desde: https://imagemagick.org/script/download.php")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Verificar iconutil solo en macOS
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
try:
|
||||||
|
# iconutil no tiene --version, mejor usar which o probar con -h
|
||||||
|
result = subprocess.run(['which', 'iconutil'], capture_output=True, check=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("✓ iconutil disponible - se crearán archivos .ico e .icns")
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
print("Error: iconutil no está disponible (solo funciona en macOS)")
|
||||||
|
print("Solo se creará el archivo .ico")
|
||||||
|
else:
|
||||||
|
print("ℹ️ Sistema no-macOS detectado - solo se creará archivo .ico")
|
||||||
|
|
||||||
|
|
||||||
|
def create_icons(input_file):
|
||||||
|
"""Crea archivos .icns e .ico a partir de un PNG"""
|
||||||
|
|
||||||
|
# Verificar que el archivo existe
|
||||||
|
if not os.path.isfile(input_file):
|
||||||
|
print(f"Error: El archivo {input_file} no existe.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Obtener información del archivo
|
||||||
|
file_path = Path(input_file)
|
||||||
|
file_dir = file_path.parent
|
||||||
|
file_name = file_path.stem # Nombre sin extensión
|
||||||
|
file_extension = file_path.suffix
|
||||||
|
|
||||||
|
if file_extension.lower() not in ['.png', '.jpg', '.jpeg', '.bmp', '.tiff']:
|
||||||
|
print(f"Advertencia: {file_extension} puede no ser compatible. Se recomienda usar PNG.")
|
||||||
|
|
||||||
|
# Crear archivo .ico usando el método simplificado
|
||||||
|
ico_output = file_dir / f"{file_name}.ico"
|
||||||
|
try:
|
||||||
|
print(f"Creando {ico_output}...")
|
||||||
|
subprocess.run([
|
||||||
|
'magick', str(input_file),
|
||||||
|
'-define', 'icon:auto-resize=256,128,64,48,32,16',
|
||||||
|
str(ico_output)
|
||||||
|
], check=True)
|
||||||
|
print(f"✓ Archivo .ico creado: {ico_output}")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Error creando archivo .ico: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Crear archivo .icns (solo en macOS)
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
try:
|
||||||
|
# Crear carpeta temporal para iconset
|
||||||
|
temp_folder = file_dir / "icon.iconset"
|
||||||
|
|
||||||
|
# Eliminar carpeta temporal si existe
|
||||||
|
if temp_folder.exists():
|
||||||
|
shutil.rmtree(temp_folder)
|
||||||
|
|
||||||
|
# Crear carpeta temporal
|
||||||
|
temp_folder.mkdir(parents=True)
|
||||||
|
|
||||||
|
# Definir los tamaños y nombres de archivo para .icns
|
||||||
|
icon_sizes = [
|
||||||
|
(16, "icon_16x16.png"),
|
||||||
|
(32, "icon_16x16@2x.png"),
|
||||||
|
(32, "icon_32x32.png"),
|
||||||
|
(64, "icon_32x32@2x.png"),
|
||||||
|
(128, "icon_128x128.png"),
|
||||||
|
(256, "icon_128x128@2x.png"),
|
||||||
|
(256, "icon_256x256.png"),
|
||||||
|
(512, "icon_256x256@2x.png"),
|
||||||
|
(512, "icon_512x512.png"),
|
||||||
|
(1024, "icon_512x512@2x.png")
|
||||||
|
]
|
||||||
|
|
||||||
|
print("Generando imágenes para .icns...")
|
||||||
|
# Crear cada tamaño de imagen
|
||||||
|
for size, output_name in icon_sizes:
|
||||||
|
output_path = temp_folder / output_name
|
||||||
|
subprocess.run([
|
||||||
|
'magick', str(input_file),
|
||||||
|
'-resize', f'{size}x{size}',
|
||||||
|
str(output_path)
|
||||||
|
], check=True)
|
||||||
|
|
||||||
|
# Crear archivo .icns usando iconutil
|
||||||
|
icns_output = file_dir / f"{file_name}.icns"
|
||||||
|
print(f"Creando {icns_output}...")
|
||||||
|
subprocess.run([
|
||||||
|
'iconutil', '-c', 'icns',
|
||||||
|
str(temp_folder),
|
||||||
|
'-o', str(icns_output)
|
||||||
|
], check=True)
|
||||||
|
|
||||||
|
# Limpiar carpeta temporal
|
||||||
|
if temp_folder.exists():
|
||||||
|
shutil.rmtree(temp_folder)
|
||||||
|
|
||||||
|
print(f"✓ Archivo .icns creado: {icns_output}")
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Error creando archivo .icns: {e}")
|
||||||
|
# Limpiar carpeta temporal en caso de error
|
||||||
|
if temp_folder.exists():
|
||||||
|
shutil.rmtree(temp_folder)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print("ℹ️ Archivo .icns no creado (solo disponible en macOS)")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Función principal"""
|
||||||
|
# Verificar argumentos
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print(f"Uso: {sys.argv[0]} ARCHIVO")
|
||||||
|
print("Ejemplo: python3 create_icons.py imagen.png")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
input_file = sys.argv[1]
|
||||||
|
|
||||||
|
# Verificar dependencias
|
||||||
|
check_dependencies()
|
||||||
|
|
||||||
|
# Crear iconos
|
||||||
|
if create_icons(input_file):
|
||||||
|
print("\n✅ Proceso completado exitosamente")
|
||||||
|
else:
|
||||||
|
print("\n❌ El proceso falló")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
release/icons/icon.afdesign
Normal file
BIN
release/icons/icon.icns
Normal file
BIN
release/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
release/icons/icon.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
42
release/macos/Info.plist
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>es</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>ViBe3 Physics</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>vibe3_physics</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>icon</string>
|
||||||
|
<key>CFBundleIconName</key>
|
||||||
|
<string>icon</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>net.jailgames.vibe3_physics</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>vibe3_physics</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CSResourcesFileMapped</key>
|
||||||
|
<true/>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>12.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>Copyright 2025 JailDesigner</string>
|
||||||
|
<key>NSPrincipalClass</key>
|
||||||
|
<string>NSApplication</string>
|
||||||
|
<key>SUPublicDSAKeyFile</key>
|
||||||
|
<string>dsa_pub.pem</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
release/windows/dll/SDL3.dll
Normal file
BIN
release/windows/dll/SDL3_ttf.dll
Normal file
BIN
release/windows/dll/libwinpthread-1.dll
Normal file
2
release/windows/vibe3.rc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// coffee.rc
|
||||||
|
IDI_ICON1 ICON "release/icons/icon.ico"
|
||||||
BIN
release/windows/vibe3.res
Normal file
23
shaders/ball.vert
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#version 450
|
||||||
|
// Per-instance data (input_rate = INSTANCE in the pipeline)
|
||||||
|
layout(location=0) in vec2 center;
|
||||||
|
layout(location=1) in vec2 halfsize;
|
||||||
|
layout(location=2) in vec4 col;
|
||||||
|
layout(location=0) out vec2 v_uv;
|
||||||
|
layout(location=1) out vec4 v_col;
|
||||||
|
void main() {
|
||||||
|
// gl_VertexIndex cycles 0..5 per instance (6 vertices = 2 triangles)
|
||||||
|
// Vertex order: TL TR BL | TR BR BL (CCW winding)
|
||||||
|
const vec2 offsets[6] = vec2[6](
|
||||||
|
vec2(-1.0, 1.0), vec2(1.0, 1.0), vec2(-1.0,-1.0),
|
||||||
|
vec2( 1.0, 1.0), vec2(1.0,-1.0), vec2(-1.0,-1.0)
|
||||||
|
);
|
||||||
|
const vec2 uvs[6] = vec2[6](
|
||||||
|
vec2(0.0,0.0), vec2(1.0,0.0), vec2(0.0,1.0),
|
||||||
|
vec2(1.0,0.0), vec2(1.0,1.0), vec2(0.0,1.0)
|
||||||
|
);
|
||||||
|
int vid = gl_VertexIndex;
|
||||||
|
gl_Position = vec4(center + offsets[vid] * halfsize, 0.0, 1.0);
|
||||||
|
v_uv = uvs[vid];
|
||||||
|
v_col = col;
|
||||||
|
}
|
||||||
24
shaders/postfx.frag
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#version 450
|
||||||
|
layout(location=0) in vec2 v_uv;
|
||||||
|
layout(location=0) out vec4 out_color;
|
||||||
|
layout(set=2, binding=0) uniform sampler2D scene;
|
||||||
|
layout(set=3, binding=0) uniform PostFXUniforms {
|
||||||
|
float vignette_strength;
|
||||||
|
float chroma_strength;
|
||||||
|
float scanline_strength;
|
||||||
|
float screen_height;
|
||||||
|
} u;
|
||||||
|
void main() {
|
||||||
|
float ca = u.chroma_strength * 0.005;
|
||||||
|
vec4 color;
|
||||||
|
color.r = texture(scene, v_uv + vec2( ca, 0.0)).r;
|
||||||
|
color.g = texture(scene, v_uv).g;
|
||||||
|
color.b = texture(scene, v_uv - vec2( ca, 0.0)).b;
|
||||||
|
color.a = texture(scene, v_uv).a;
|
||||||
|
float scan = 0.85 + 0.15 * sin(v_uv.y * 3.14159265 * u.screen_height);
|
||||||
|
color.rgb *= mix(1.0, scan, u.scanline_strength);
|
||||||
|
vec2 d = v_uv - vec2(0.5, 0.5);
|
||||||
|
float vignette = 1.0 - dot(d, d) * u.vignette_strength;
|
||||||
|
color.rgb *= clamp(vignette, 0.0, 1.0);
|
||||||
|
out_color = color;
|
||||||
|
}
|
||||||
10
shaders/postfx.vert
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#version 450
|
||||||
|
layout(location=0) out vec2 v_uv;
|
||||||
|
void main() {
|
||||||
|
// Full-screen triangle from vertex index (no vertex buffer needed)
|
||||||
|
// NDC/UV mapping matches the MSL version (SDL3 GPU normalizes Y-up on all backends)
|
||||||
|
vec2 positions[3] = vec2[3](vec2(-1.0,-1.0), vec2(3.0,-1.0), vec2(-1.0,3.0));
|
||||||
|
vec2 uvs[3] = vec2[3](vec2(0.0, 1.0), vec2(2.0, 1.0), vec2(0.0,-1.0));
|
||||||
|
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
|
||||||
|
v_uv = uvs[gl_VertexIndex];
|
||||||
|
}
|
||||||
5
shaders/precompiled/ball_vert_spv.h
Normal file
5
shaders/precompiled/postfx_frag_spv.h
Normal file
5
shaders/precompiled/postfx_vert_spv.h
Normal file
5
shaders/precompiled/sprite_frag_spv.h
Normal file
5
shaders/precompiled/sprite_vert_spv.h
Normal file
9
shaders/sprite.frag
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#version 450
|
||||||
|
layout(location=0) in vec2 v_uv;
|
||||||
|
layout(location=1) in vec4 v_col;
|
||||||
|
layout(location=0) out vec4 out_color;
|
||||||
|
layout(set=2, binding=0) uniform sampler2D tex;
|
||||||
|
void main() {
|
||||||
|
vec4 t = texture(tex, v_uv);
|
||||||
|
out_color = vec4(t.rgb * v_col.rgb, t.a * v_col.a);
|
||||||
|
}
|
||||||
11
shaders/sprite.vert
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#version 450
|
||||||
|
layout(location=0) in vec2 pos;
|
||||||
|
layout(location=1) in vec2 uv;
|
||||||
|
layout(location=2) in vec4 col;
|
||||||
|
layout(location=0) out vec2 v_uv;
|
||||||
|
layout(location=1) out vec4 v_col;
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(pos, 0.0, 1.0);
|
||||||
|
v_uv = uv;
|
||||||
|
v_col = col;
|
||||||
|
}
|
||||||
185
source/ball.cpp
@@ -1,68 +1,65 @@
|
|||||||
#include "ball.h"
|
#include "ball.hpp"
|
||||||
|
|
||||||
#include <stdlib.h> // for rand
|
#include <algorithm>
|
||||||
|
#include <cmath> // for fabs
|
||||||
|
#include <cstdlib> // for rand
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include <cmath> // for fabs
|
#include "defines.hpp" // for Color, SCREEN_HEIGHT, GRAVITY_FORCE
|
||||||
|
|
||||||
#include "defines.h" // for BALL_SIZE, Color, SCREEN_HEIGHT, GRAVITY_FORCE
|
|
||||||
class Texture;
|
class Texture;
|
||||||
|
|
||||||
// Función auxiliar para generar pérdida aleatoria en rebotes
|
// Función auxiliar para generar pérdida aleatoria en rebotes
|
||||||
float generateBounceVariation() {
|
auto generateBounceVariation() -> float {
|
||||||
// Genera un valor entre 0 y BOUNCE_RANDOM_LOSS_PERCENT (solo pérdida adicional)
|
// Genera un valor entre 0 y BOUNCE_RANDOM_LOSS_PERCENT (solo pérdida adicional)
|
||||||
float loss = (rand() % 1000) / 1000.0f * BOUNCE_RANDOM_LOSS_PERCENT;
|
float loss = (rand() % 1000) / 1000.0f * BOUNCE_RANDOM_LOSS_PERCENT;
|
||||||
return 1.0f - loss; // Retorna multiplicador (ej: 0.90 - 1.00 para 10% max pérdida)
|
return 1.0f - loss; // Retorna multiplicador (ej: 0.90 - 1.00 para 10% max pérdida)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Función auxiliar para generar pérdida lateral aleatoria
|
// Función auxiliar para generar pérdida lateral aleatoria
|
||||||
float generateLateralLoss() {
|
auto generateLateralLoss() -> float {
|
||||||
// Genera un valor entre 0 y LATERAL_LOSS_PERCENT
|
// Genera un valor entre 0 y LATERAL_LOSS_PERCENT
|
||||||
float loss = (rand() % 1000) / 1000.0f * LATERAL_LOSS_PERCENT;
|
float loss = (rand() % 1000) / 1000.0f * LATERAL_LOSS_PERCENT;
|
||||||
return 1.0f - loss; // Retorna multiplicador (ej: 0.98 - 1.0 para 0-2% pérdida)
|
return 1.0f - loss; // Retorna multiplicador (ej: 0.98 - 1.0 para 0-2% pérdida)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
Ball::Ball(float x, float vx, float vy, Color color, std::shared_ptr<Texture> texture, int screen_width, int screen_height, GravityDirection gravity_dir, float mass_factor)
|
Ball::Ball(float x, float y, float vx, float vy, Color color, const std::shared_ptr<Texture>& texture, int screen_width, int screen_height, int ball_size, GravityDirection gravity_dir, float mass_factor)
|
||||||
: sprite_(std::make_unique<Sprite>(texture)),
|
: sprite_(std::make_unique<Sprite>(texture)),
|
||||||
pos_({x, 0.0f, BALL_SIZE, BALL_SIZE}) {
|
pos_({.x = x, .y = y, .w = static_cast<float>(ball_size), .h = static_cast<float>(ball_size)}) {
|
||||||
// Convertir velocidades de píxeles/frame a píxeles/segundo (multiplicar por 60)
|
// Convertir velocidades de píxeles/frame a píxeles/segundo (multiplicar por 60)
|
||||||
vx_ = vx * 60.0f;
|
vx_ = vx * 60.0f;
|
||||||
vy_ = vy * 60.0f;
|
vy_ = vy * 60.0f;
|
||||||
sprite_->setPos({pos_.x, pos_.y});
|
sprite_->setPos({pos_.x, pos_.y});
|
||||||
sprite_->setSize(BALL_SIZE, BALL_SIZE);
|
sprite_->setSize(ball_size, ball_size);
|
||||||
sprite_->setClip({0, 0, BALL_SIZE, BALL_SIZE});
|
sprite_->setClip({0.0f, 0.0f, static_cast<float>(ball_size), static_cast<float>(ball_size)});
|
||||||
color_ = color;
|
color_ = color;
|
||||||
// Convertir gravedad de píxeles/frame² a píxeles/segundo² (multiplicar por 60²)
|
// Convertir gravedad de píxeles/frame² a píxeles/segundo² (multiplicar por 60²)
|
||||||
gravity_force_ = GRAVITY_FORCE * 60.0f * 60.0f;
|
gravity_force_ = GRAVITY_FORCE * 60.0f * 60.0f;
|
||||||
gravity_mass_factor_ = mass_factor; // Factor de masa individual para esta pelota
|
gravity_mass_factor_ = mass_factor; // Factor de masa individual para esta pelota
|
||||||
gravity_direction_ = gravity_dir;
|
gravity_direction_ = gravity_dir;
|
||||||
screen_width_ = screen_width; // Dimensiones del terreno de juego
|
screen_width_ = screen_width; // Dimensiones del terreno de juego
|
||||||
screen_height_ = screen_height;
|
screen_height_ = screen_height;
|
||||||
on_surface_ = false;
|
on_surface_ = false;
|
||||||
stopped_ = false;
|
|
||||||
// Coeficiente base IGUAL para todas las pelotas (solo variación por rebote individual)
|
// Coeficiente base IGUAL para todas las pelotas (solo variación por rebote individual)
|
||||||
loss_ = BASE_BOUNCE_COEFFICIENT; // Coeficiente fijo para todas las pelotas
|
loss_ = BASE_BOUNCE_COEFFICIENT; // Coeficiente fijo para todas las pelotas
|
||||||
|
|
||||||
// Inicializar valores RotoBall
|
// Inicializar valores Shape (figuras 3D)
|
||||||
pos_3d_x_ = 0.0f;
|
pos_3d_x_ = 0.0f;
|
||||||
pos_3d_y_ = 0.0f;
|
pos_3d_y_ = 0.0f;
|
||||||
pos_3d_z_ = 0.0f;
|
pos_3d_z_ = 0.0f;
|
||||||
target_x_ = pos_.x;
|
target_x_ = pos_.x;
|
||||||
target_y_ = pos_.y;
|
target_y_ = pos_.y;
|
||||||
depth_brightness_ = 1.0f;
|
depth_brightness_ = 1.0f;
|
||||||
rotoball_attraction_active_ = false;
|
depth_scale_ = 1.0f;
|
||||||
|
shape_attraction_active_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualiza la lógica de la clase
|
// Actualiza la lógica de la clase
|
||||||
void Ball::update(float deltaTime) {
|
void Ball::update(float delta_time) { // NOLINT(readability-function-cognitive-complexity)
|
||||||
if (stopped_) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aplica la gravedad según la dirección (píxeles/segundo²)
|
// Aplica la gravedad según la dirección (píxeles/segundo²)
|
||||||
if (!on_surface_) {
|
if (!on_surface_) {
|
||||||
// Aplicar gravedad multiplicada por factor de masa individual
|
// Aplicar gravedad multiplicada por factor de masa individual
|
||||||
float effective_gravity = gravity_force_ * gravity_mass_factor_ * deltaTime;
|
float effective_gravity = gravity_force_ * gravity_mass_factor_ * delta_time;
|
||||||
switch (gravity_direction_) {
|
switch (gravity_direction_) {
|
||||||
case GravityDirection::DOWN:
|
case GravityDirection::DOWN:
|
||||||
vy_ += effective_gravity;
|
vy_ += effective_gravity;
|
||||||
@@ -81,26 +78,26 @@ void Ball::update(float deltaTime) {
|
|||||||
|
|
||||||
// Actualiza la posición en función de la velocidad (píxeles/segundo)
|
// Actualiza la posición en función de la velocidad (píxeles/segundo)
|
||||||
if (!on_surface_) {
|
if (!on_surface_) {
|
||||||
pos_.x += vx_ * deltaTime;
|
pos_.x += vx_ * delta_time;
|
||||||
pos_.y += vy_ * deltaTime;
|
pos_.y += vy_ * delta_time;
|
||||||
} else {
|
} else {
|
||||||
// Si está en superficie, mantener posición según dirección de gravedad
|
// Si está en superficie, mantener posición según dirección de gravedad
|
||||||
switch (gravity_direction_) {
|
switch (gravity_direction_) {
|
||||||
case GravityDirection::DOWN:
|
case GravityDirection::DOWN:
|
||||||
pos_.y = screen_height_ - pos_.h;
|
pos_.y = screen_height_ - pos_.h;
|
||||||
pos_.x += vx_ * deltaTime; // Seguir moviéndose en X
|
pos_.x += vx_ * delta_time; // Seguir moviéndose en X
|
||||||
break;
|
break;
|
||||||
case GravityDirection::UP:
|
case GravityDirection::UP:
|
||||||
pos_.y = 0;
|
pos_.y = 0;
|
||||||
pos_.x += vx_ * deltaTime; // Seguir moviéndose en X
|
pos_.x += vx_ * delta_time; // Seguir moviéndose en X
|
||||||
break;
|
break;
|
||||||
case GravityDirection::LEFT:
|
case GravityDirection::LEFT:
|
||||||
pos_.x = 0;
|
pos_.x = 0;
|
||||||
pos_.y += vy_ * deltaTime; // Seguir moviéndose en Y
|
pos_.y += vy_ * delta_time; // Seguir moviéndose en Y
|
||||||
break;
|
break;
|
||||||
case GravityDirection::RIGHT:
|
case GravityDirection::RIGHT:
|
||||||
pos_.x = screen_width_ - pos_.w;
|
pos_.x = screen_width_ - pos_.w;
|
||||||
pos_.y += vy_ * deltaTime; // Seguir moviéndose en Y
|
pos_.y += vy_ * delta_time; // Seguir moviéndose en Y
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,7 +177,7 @@ void Ball::update(float deltaTime) {
|
|||||||
// Aplica rozamiento al estar en superficie
|
// Aplica rozamiento al estar en superficie
|
||||||
if (on_surface_) {
|
if (on_surface_) {
|
||||||
// Convertir rozamiento de frame-based a time-based
|
// Convertir rozamiento de frame-based a time-based
|
||||||
float friction_factor = pow(0.97f, 60.0f * deltaTime);
|
float friction_factor = std::pow(0.97f, 60.0f * delta_time);
|
||||||
|
|
||||||
switch (gravity_direction_) {
|
switch (gravity_direction_) {
|
||||||
case GravityDirection::DOWN:
|
case GravityDirection::DOWN:
|
||||||
@@ -189,7 +186,6 @@ void Ball::update(float deltaTime) {
|
|||||||
vx_ = vx_ * friction_factor;
|
vx_ = vx_ * friction_factor;
|
||||||
if (std::fabs(vx_) < 6.0f) {
|
if (std::fabs(vx_) < 6.0f) {
|
||||||
vx_ = 0.0f;
|
vx_ = 0.0f;
|
||||||
stopped_ = true;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case GravityDirection::LEFT:
|
case GravityDirection::LEFT:
|
||||||
@@ -198,7 +194,6 @@ void Ball::update(float deltaTime) {
|
|||||||
vy_ = vy_ * friction_factor;
|
vy_ = vy_ * friction_factor;
|
||||||
if (std::fabs(vy_) < 6.0f) {
|
if (std::fabs(vy_) < 6.0f) {
|
||||||
vy_ = 0.0f;
|
vy_ = 0.0f;
|
||||||
stopped_ = true;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -219,7 +214,6 @@ void Ball::modVel(float vx, float vy) {
|
|||||||
vx_ = vx_ + (vx * 60.0f); // Convertir a píxeles/segundo
|
vx_ = vx_ + (vx * 60.0f); // Convertir a píxeles/segundo
|
||||||
vy_ = vy_ + (vy * 60.0f); // Convertir a píxeles/segundo
|
vy_ = vy_ + (vy * 60.0f); // Convertir a píxeles/segundo
|
||||||
on_surface_ = false;
|
on_surface_ = false;
|
||||||
stopped_ = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cambia la gravedad (usa la versión convertida)
|
// Cambia la gravedad (usa la versión convertida)
|
||||||
@@ -227,17 +221,33 @@ void Ball::switchGravity() {
|
|||||||
gravity_force_ = gravity_force_ == 0.0f ? (GRAVITY_FORCE * 60.0f * 60.0f) : 0.0f;
|
gravity_force_ = gravity_force_ == 0.0f ? (GRAVITY_FORCE * 60.0f * 60.0f) : 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reactiva la gravedad si está desactivada
|
||||||
|
void Ball::enableGravityIfDisabled() {
|
||||||
|
if (gravity_force_ == 0.0f) {
|
||||||
|
gravity_force_ = GRAVITY_FORCE * 60.0f * 60.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fuerza gravedad ON (siempre activa)
|
||||||
|
void Ball::forceGravityOn() {
|
||||||
|
gravity_force_ = GRAVITY_FORCE * 60.0f * 60.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fuerza gravedad OFF (siempre desactiva)
|
||||||
|
void Ball::forceGravityOff() {
|
||||||
|
gravity_force_ = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
// Cambia la dirección de gravedad
|
// Cambia la dirección de gravedad
|
||||||
void Ball::setGravityDirection(GravityDirection direction) {
|
void Ball::setGravityDirection(GravityDirection direction) {
|
||||||
gravity_direction_ = direction;
|
gravity_direction_ = direction;
|
||||||
on_surface_ = false; // Ya no está en superficie al cambiar dirección
|
on_surface_ = false; // Ya no está en superficie al cambiar dirección
|
||||||
stopped_ = false; // Reactivar movimiento
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplica un pequeño empuje lateral aleatorio
|
// Aplica un pequeño empuje lateral aleatorio
|
||||||
void Ball::applyRandomLateralPush() {
|
void Ball::applyRandomLateralPush() {
|
||||||
// Generar velocidad lateral aleatoria (nunca 0)
|
// Generar velocidad lateral aleatoria (nunca 0)
|
||||||
float lateral_speed = GRAVITY_CHANGE_LATERAL_MIN + (rand() % 1000) / 1000.0f * (GRAVITY_CHANGE_LATERAL_MAX - GRAVITY_CHANGE_LATERAL_MIN);
|
float lateral_speed = GRAVITY_CHANGE_LATERAL_MIN + ((rand() % 1000) / 1000.0f * (GRAVITY_CHANGE_LATERAL_MAX - GRAVITY_CHANGE_LATERAL_MIN));
|
||||||
|
|
||||||
// Signo aleatorio (+ o -)
|
// Signo aleatorio (+ o -)
|
||||||
int sign = ((rand() % 2) * 2) - 1;
|
int sign = ((rand() % 2) * 2) - 1;
|
||||||
@@ -258,19 +268,19 @@ void Ball::applyRandomLateralPush() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Funciones para modo RotoBall
|
// Funciones para modo Shape (figuras 3D)
|
||||||
void Ball::setRotoBallPosition3D(float x, float y, float z) {
|
void Ball::setShapePosition3D(float x, float y, float z) {
|
||||||
pos_3d_x_ = x;
|
pos_3d_x_ = x;
|
||||||
pos_3d_y_ = y;
|
pos_3d_y_ = y;
|
||||||
pos_3d_z_ = z;
|
pos_3d_z_ = z;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Ball::setRotoBallTarget2D(float x, float y) {
|
void Ball::setShapeTarget2D(float x, float y) {
|
||||||
target_x_ = x;
|
target_x_ = x;
|
||||||
target_y_ = y;
|
target_y_ = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Ball::setRotoBallScreenPosition(float x, float y) {
|
void Ball::setShapeScreenPosition(float x, float y) {
|
||||||
pos_.x = x;
|
pos_.x = x;
|
||||||
pos_.y = y;
|
pos_.y = y;
|
||||||
sprite_->setPos({x, y});
|
sprite_->setPos({x, y});
|
||||||
@@ -280,30 +290,61 @@ void Ball::setDepthBrightness(float brightness) {
|
|||||||
depth_brightness_ = brightness;
|
depth_brightness_ = brightness;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activar/desactivar atracción física hacia esfera RotoBall
|
void Ball::setDepthScale(float scale) {
|
||||||
void Ball::enableRotoBallAttraction(bool enable) {
|
depth_scale_ = scale;
|
||||||
rotoball_attraction_active_ = enable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplicar fuerza de resorte hacia punto objetivo en esfera rotante
|
// Activar/desactivar atracción física hacia figuras 3D
|
||||||
void Ball::applyRotoBallForce(float target_x, float target_y, float deltaTime) {
|
void Ball::enableShapeAttraction(bool enable) {
|
||||||
if (!rotoball_attraction_active_) return;
|
shape_attraction_active_ = enable;
|
||||||
|
|
||||||
|
// Al activar atracción, resetear flags de superficie para permitir física completa
|
||||||
|
if (enable) {
|
||||||
|
on_surface_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener distancia actual al punto objetivo (para calcular convergencia)
|
||||||
|
auto Ball::getDistanceToTarget() const -> float {
|
||||||
|
// Siempre calcular distancia (útil para convergencia en LOGO mode)
|
||||||
|
float dx = target_x_ - pos_.x;
|
||||||
|
float dy = target_y_ - pos_.y;
|
||||||
|
return sqrtf((dx * dx) + (dy * dy));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar fuerza de resorte hacia punto objetivo en figuras 3D
|
||||||
|
void Ball::applyShapeForce(float target_x, float target_y, float sphere_radius, float delta_time, float spring_k_base, float damping_base_base, float damping_near_base, float near_threshold_base, float max_force_base) {
|
||||||
|
if (!shape_attraction_active_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular factor de escala basado en el radio (radio base = 80px)
|
||||||
|
// Si radius=80 → scale=1.0, si radius=160 → scale=2.0, si radius=360 → scale=4.5
|
||||||
|
const float BASE_RADIUS = 80.0f;
|
||||||
|
float scale = sphere_radius / BASE_RADIUS;
|
||||||
|
|
||||||
|
// Escalar constantes de física proporcionalmente
|
||||||
|
float spring_k = spring_k_base * scale;
|
||||||
|
float damping_base = damping_base_base * scale;
|
||||||
|
float damping_near = damping_near_base * scale;
|
||||||
|
float near_threshold = near_threshold_base * scale;
|
||||||
|
float max_force = max_force_base * scale;
|
||||||
|
|
||||||
// Calcular vector diferencia (dirección hacia el target)
|
// Calcular vector diferencia (dirección hacia el target)
|
||||||
float diff_x = target_x - pos_.x;
|
float diff_x = target_x - pos_.x;
|
||||||
float diff_y = target_y - pos_.y;
|
float diff_y = target_y - pos_.y;
|
||||||
|
|
||||||
// Calcular distancia al punto objetivo
|
// Calcular distancia al punto objetivo
|
||||||
float distance = sqrtf(diff_x * diff_x + diff_y * diff_y);
|
float distance = sqrtf((diff_x * diff_x) + (diff_y * diff_y));
|
||||||
|
|
||||||
// Fuerza de resorte (Ley de Hooke: F = -k * x)
|
// Fuerza de resorte (Ley de Hooke: F = -k * x)
|
||||||
float spring_force_x = ROTOBALL_SPRING_K * diff_x;
|
float spring_force_x = spring_k * diff_x;
|
||||||
float spring_force_y = ROTOBALL_SPRING_K * diff_y;
|
float spring_force_y = spring_k * diff_y;
|
||||||
|
|
||||||
// Amortiguación variable: más cerca del punto = más amortiguación (estabilización)
|
// Amortiguación variable: más cerca del punto = más amortiguación (estabilización)
|
||||||
float damping = (distance < ROTOBALL_NEAR_THRESHOLD)
|
float damping = (distance < near_threshold)
|
||||||
? ROTOBALL_DAMPING_NEAR
|
? damping_near
|
||||||
: ROTOBALL_DAMPING_BASE;
|
: damping_base;
|
||||||
|
|
||||||
// Fuerza de amortiguación (proporcional a la velocidad)
|
// Fuerza de amortiguación (proporcional a la velocidad)
|
||||||
float damping_force_x = damping * vx_;
|
float damping_force_x = damping * vx_;
|
||||||
@@ -314,28 +355,48 @@ void Ball::applyRotoBallForce(float target_x, float target_y, float deltaTime) {
|
|||||||
float total_force_y = spring_force_y - damping_force_y;
|
float total_force_y = spring_force_y - damping_force_y;
|
||||||
|
|
||||||
// Limitar magnitud de fuerza (evitar explosiones numéricas)
|
// Limitar magnitud de fuerza (evitar explosiones numéricas)
|
||||||
float force_magnitude = sqrtf(total_force_x * total_force_x + total_force_y * total_force_y);
|
float force_magnitude = sqrtf((total_force_x * total_force_x) + (total_force_y * total_force_y));
|
||||||
if (force_magnitude > ROTOBALL_MAX_FORCE) {
|
if (force_magnitude > max_force) {
|
||||||
float scale = ROTOBALL_MAX_FORCE / force_magnitude;
|
float scale_limit = max_force / force_magnitude;
|
||||||
total_force_x *= scale;
|
total_force_x *= scale_limit;
|
||||||
total_force_y *= scale;
|
total_force_y *= scale_limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplicar aceleración (F = ma, asumiendo m = 1 para simplificar)
|
// Aplicar aceleración (F = ma, asumiendo m = 1 para simplificar)
|
||||||
// a = F/m, pero m=1, así que a = F
|
// a = F/m, pero m=1, así que a = F
|
||||||
vx_ += total_force_x * deltaTime;
|
vx_ += total_force_x * delta_time;
|
||||||
vy_ += total_force_y * deltaTime;
|
vy_ += total_force_y * delta_time;
|
||||||
|
|
||||||
// Actualizar posición con física normal (velocidad integrada)
|
// Actualizar posición con física normal (velocidad integrada)
|
||||||
pos_.x += vx_ * deltaTime;
|
pos_.x += vx_ * delta_time;
|
||||||
pos_.y += vy_ * deltaTime;
|
pos_.y += vy_ * delta_time;
|
||||||
|
|
||||||
// Mantener pelotas dentro de los límites de pantalla
|
// Mantener pelotas dentro de los límites de pantalla
|
||||||
if (pos_.x < 0) pos_.x = 0;
|
pos_.x = std::max<float>(pos_.x, 0);
|
||||||
if (pos_.x + pos_.w > screen_width_) pos_.x = screen_width_ - pos_.w;
|
if (pos_.x + pos_.w > screen_width_) {
|
||||||
if (pos_.y < 0) pos_.y = 0;
|
pos_.x = screen_width_ - pos_.w;
|
||||||
if (pos_.y + pos_.h > screen_height_) pos_.y = screen_height_ - pos_.h;
|
}
|
||||||
|
pos_.y = std::max<float>(pos_.y, 0);
|
||||||
|
if (pos_.y + pos_.h > screen_height_) {
|
||||||
|
pos_.y = screen_height_ - pos_.h;
|
||||||
|
}
|
||||||
|
|
||||||
// Actualizar sprite para renderizado
|
// Actualizar sprite para renderizado
|
||||||
sprite_->setPos({pos_.x, pos_.y});
|
sprite_->setPos({pos_.x, pos_.y});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sistema de cambio de sprite dinámico
|
||||||
|
void Ball::updateSize(int new_size) {
|
||||||
|
// Actualizar tamaño del hitbox
|
||||||
|
pos_.w = static_cast<float>(new_size);
|
||||||
|
pos_.h = static_cast<float>(new_size);
|
||||||
|
|
||||||
|
// Actualizar sprite
|
||||||
|
sprite_->setSize(new_size, new_size);
|
||||||
|
sprite_->setClip({0.0f, 0.0f, static_cast<float>(new_size), static_cast<float>(new_size)});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Ball::setTexture(std::shared_ptr<Texture> texture) {
|
||||||
|
// Actualizar textura del sprite
|
||||||
|
sprite_->setTexture(std::move(texture));
|
||||||
}
|
}
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <SDL3/SDL_rect.h> // for SDL_FRect
|
|
||||||
|
|
||||||
#include <memory> // for shared_ptr, unique_ptr
|
|
||||||
|
|
||||||
#include "defines.h" // for Color
|
|
||||||
#include "external/sprite.h" // for Sprite
|
|
||||||
class Texture;
|
|
||||||
|
|
||||||
class Ball {
|
|
||||||
private:
|
|
||||||
std::unique_ptr<Sprite> sprite_; // Sprite para pintar la clase
|
|
||||||
SDL_FRect pos_; // Posición y tamaño de la pelota
|
|
||||||
float vx_, vy_; // Velocidad
|
|
||||||
float gravity_force_; // Gravedad base
|
|
||||||
float gravity_mass_factor_; // Factor de masa individual (0.7-1.3, afecta gravedad)
|
|
||||||
GravityDirection gravity_direction_; // Direcci\u00f3n de la gravedad
|
|
||||||
int screen_width_; // Ancho del terreno de juego
|
|
||||||
int screen_height_; // Alto del terreno de juego
|
|
||||||
Color color_; // Color de la pelota
|
|
||||||
bool on_surface_; // Indica si la pelota est\u00e1 en la superficie (suelo/techo/pared)
|
|
||||||
bool stopped_; // Indica si la pelota ha terminado de moverse;
|
|
||||||
float loss_; // Coeficiente de rebote. Pérdida de energía en cada rebote
|
|
||||||
|
|
||||||
// Datos para modo RotoBall (esfera 3D)
|
|
||||||
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
|
|
||||||
Ball(float x, float vx, float vy, Color color, std::shared_ptr<Texture> texture, int screen_width, int screen_height, GravityDirection gravity_dir = GravityDirection::DOWN, float mass_factor = 1.0f);
|
|
||||||
|
|
||||||
// Destructor
|
|
||||||
~Ball() = default;
|
|
||||||
|
|
||||||
// Actualiza la lógica de la clase
|
|
||||||
void update(float deltaTime);
|
|
||||||
|
|
||||||
// Pinta la clase
|
|
||||||
void render();
|
|
||||||
|
|
||||||
// Modifica la velocidad
|
|
||||||
void modVel(float vx, float vy);
|
|
||||||
|
|
||||||
// Cambia la gravedad
|
|
||||||
void switchGravity();
|
|
||||||
|
|
||||||
// Cambia la direcci\u00f3n de gravedad
|
|
||||||
void setGravityDirection(GravityDirection direction);
|
|
||||||
|
|
||||||
// Aplica un peque\u00f1o empuje lateral aleatorio
|
|
||||||
void applyRandomLateralPush();
|
|
||||||
|
|
||||||
// Getters para debug
|
|
||||||
float getVelocityY() const { return vy_; }
|
|
||||||
float getVelocityX() const { return vx_; }
|
|
||||||
float getGravityForce() const { return gravity_force_; }
|
|
||||||
float getLossCoefficient() const { return loss_; }
|
|
||||||
GravityDirection getGravityDirection() const { return gravity_direction_; }
|
|
||||||
bool isOnSurface() const { return on_surface_; }
|
|
||||||
bool isStopped() const { return stopped_; }
|
|
||||||
|
|
||||||
// Getters para batch rendering
|
|
||||||
SDL_FRect getPosition() const { return pos_; }
|
|
||||||
Color getColor() const { return color_; }
|
|
||||||
|
|
||||||
// Funciones para modo RotoBall
|
|
||||||
void setRotoBallPosition3D(float x, float y, float z);
|
|
||||||
void setRotoBallTarget2D(float x, float y);
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
112
source/ball.hpp
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_rect.h> // for SDL_FRect
|
||||||
|
|
||||||
|
#include <memory> // for shared_ptr, unique_ptr
|
||||||
|
|
||||||
|
#include "defines.hpp" // for Color
|
||||||
|
#include "external/sprite.hpp" // for Sprite
|
||||||
|
class Texture;
|
||||||
|
|
||||||
|
class Ball {
|
||||||
|
private:
|
||||||
|
std::unique_ptr<Sprite> sprite_; // Sprite para pintar la clase
|
||||||
|
SDL_FRect pos_; // Posición y tamaño de la pelota
|
||||||
|
float vx_, vy_; // Velocidad
|
||||||
|
float gravity_force_; // Gravedad base
|
||||||
|
float gravity_mass_factor_; // Factor de masa individual (0.7-1.3, afecta gravedad)
|
||||||
|
GravityDirection gravity_direction_; // Direcci\u00f3n de la gravedad
|
||||||
|
int screen_width_; // Ancho del terreno de juego
|
||||||
|
int screen_height_; // Alto del terreno de juego
|
||||||
|
Color color_; // Color de la pelota
|
||||||
|
bool on_surface_; // Indica si la pelota est\u00e1 en la superficie (suelo/techo/pared)
|
||||||
|
float loss_; // Coeficiente de rebote. Pérdida de energía en cada rebote
|
||||||
|
|
||||||
|
// Datos para modo Shape (figuras 3D)
|
||||||
|
float pos_3d_x_, pos_3d_y_, pos_3d_z_; // Posición 3D en la figura
|
||||||
|
float target_x_, target_y_; // Posición destino 2D (proyección)
|
||||||
|
float depth_brightness_; // Brillo según profundidad Z (0.0-1.0)
|
||||||
|
float depth_scale_; // Escala según profundidad Z (0.5-1.5)
|
||||||
|
bool shape_attraction_active_; // ¿Está siendo atraída hacia la figura?
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Constructor
|
||||||
|
Ball(float x, float y, float vx, float vy, Color color, const std::shared_ptr<Texture>& texture, int screen_width, int screen_height, int ball_size, GravityDirection gravity_dir = GravityDirection::DOWN, float mass_factor = 1.0f);
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
~Ball() = default;
|
||||||
|
|
||||||
|
// Actualiza la lógica de la clase
|
||||||
|
void update(float delta_time);
|
||||||
|
|
||||||
|
// Pinta la clase
|
||||||
|
void render();
|
||||||
|
|
||||||
|
// Modifica la velocidad
|
||||||
|
void modVel(float vx, float vy);
|
||||||
|
|
||||||
|
// Cambia la gravedad
|
||||||
|
void switchGravity();
|
||||||
|
|
||||||
|
// Reactiva la gravedad si está desactivada
|
||||||
|
void enableGravityIfDisabled();
|
||||||
|
|
||||||
|
// Fuerza gravedad ON (siempre activa)
|
||||||
|
void forceGravityOn();
|
||||||
|
|
||||||
|
// Fuerza gravedad OFF (siempre desactiva)
|
||||||
|
void forceGravityOff();
|
||||||
|
|
||||||
|
// Cambia la direcci\u00f3n de gravedad
|
||||||
|
void setGravityDirection(GravityDirection direction);
|
||||||
|
|
||||||
|
// Aplica un peque\u00f1o empuje lateral aleatorio
|
||||||
|
void applyRandomLateralPush();
|
||||||
|
|
||||||
|
// Getters para debug
|
||||||
|
float getVelocityY() const { return vy_; }
|
||||||
|
float getVelocityX() const { return vx_; }
|
||||||
|
float getGravityForce() const { return gravity_force_; }
|
||||||
|
float getLossCoefficient() const { return loss_; }
|
||||||
|
GravityDirection getGravityDirection() const { return gravity_direction_; }
|
||||||
|
bool isOnSurface() const { return on_surface_; }
|
||||||
|
|
||||||
|
// Getters/Setters para velocidad (usado por BoidManager)
|
||||||
|
void getVelocity(float& vx, float& vy) const {
|
||||||
|
vx = vx_;
|
||||||
|
vy = vy_;
|
||||||
|
}
|
||||||
|
void setVelocity(float vx, float vy) {
|
||||||
|
vx_ = vx;
|
||||||
|
vy_ = vy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setter para posición simple (usado por BoidManager)
|
||||||
|
void setPosition(float x, float y) {
|
||||||
|
pos_.x = x;
|
||||||
|
pos_.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters/Setters para batch rendering
|
||||||
|
SDL_FRect getPosition() const { return pos_; }
|
||||||
|
Color getColor() const { return color_; }
|
||||||
|
void setColor(const Color& color) { color_ = color; }
|
||||||
|
|
||||||
|
// Sistema de cambio de sprite dinámico
|
||||||
|
void updateSize(int new_size); // Actualizar tamaño de hitbox
|
||||||
|
void setTexture(std::shared_ptr<Texture> texture); // Cambiar textura del sprite
|
||||||
|
|
||||||
|
// Funciones para modo Shape (figuras 3D)
|
||||||
|
void setShapePosition3D(float x, float y, float z);
|
||||||
|
void setShapeTarget2D(float x, float y);
|
||||||
|
void setShapeScreenPosition(float x, float y); // Establecer posición directa en pantalla
|
||||||
|
void setDepthBrightness(float brightness);
|
||||||
|
float getDepthBrightness() const { return depth_brightness_; }
|
||||||
|
void setDepthScale(float scale);
|
||||||
|
float getDepthScale() const { return depth_scale_; }
|
||||||
|
|
||||||
|
// Sistema de atracción física hacia figuras 3D
|
||||||
|
void enableShapeAttraction(bool enable);
|
||||||
|
float getDistanceToTarget() const; // Distancia actual al punto objetivo
|
||||||
|
void applyShapeForce(float target_x, float target_y, float sphere_radius, float delta_time, float spring_k_base = SHAPE_SPRING_K, float damping_base_base = SHAPE_DAMPING_BASE, float damping_near_base = SHAPE_DAMPING_NEAR, float near_threshold_base = SHAPE_NEAR_THRESHOLD, float max_force_base = SHAPE_MAX_FORCE);
|
||||||
|
};
|
||||||
421
source/boids_mgr/boid_manager.cpp
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
#include "boid_manager.hpp"
|
||||||
|
|
||||||
|
#include <algorithm> // for std::min, std::max
|
||||||
|
#include <cmath> // for sqrt, atan2
|
||||||
|
|
||||||
|
#include "ball.hpp" // for Ball
|
||||||
|
#include "engine.hpp" // for Engine (si se necesita)
|
||||||
|
#include "scene/scene_manager.hpp" // for SceneManager
|
||||||
|
#include "state/state_manager.hpp" // for StateManager
|
||||||
|
#include "ui/ui_manager.hpp" // for UIManager
|
||||||
|
|
||||||
|
BoidManager::BoidManager()
|
||||||
|
: engine_(nullptr),
|
||||||
|
scene_mgr_(nullptr),
|
||||||
|
ui_mgr_(nullptr),
|
||||||
|
state_mgr_(nullptr),
|
||||||
|
screen_width_(0),
|
||||||
|
screen_height_(0),
|
||||||
|
boids_active_(false),
|
||||||
|
spatial_grid_(800, 600, BOID_GRID_CELL_SIZE) // Tamaño por defecto, se actualiza en initialize()
|
||||||
|
,
|
||||||
|
separation_radius_(BOID_SEPARATION_RADIUS),
|
||||||
|
alignment_radius_(BOID_ALIGNMENT_RADIUS),
|
||||||
|
cohesion_radius_(BOID_COHESION_RADIUS),
|
||||||
|
separation_weight_(BOID_SEPARATION_WEIGHT),
|
||||||
|
alignment_weight_(BOID_ALIGNMENT_WEIGHT),
|
||||||
|
cohesion_weight_(BOID_COHESION_WEIGHT),
|
||||||
|
max_speed_(BOID_MAX_SPEED),
|
||||||
|
min_speed_(BOID_MIN_SPEED),
|
||||||
|
max_force_(BOID_MAX_FORCE),
|
||||||
|
boundary_margin_(BOID_BOUNDARY_MARGIN),
|
||||||
|
boundary_weight_(BOID_BOUNDARY_WEIGHT) {
|
||||||
|
}
|
||||||
|
|
||||||
|
BoidManager::~BoidManager() = default;
|
||||||
|
|
||||||
|
void BoidManager::initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, StateManager* state_mgr, int screen_width, int screen_height) {
|
||||||
|
engine_ = engine;
|
||||||
|
scene_mgr_ = scene_mgr;
|
||||||
|
ui_mgr_ = ui_mgr;
|
||||||
|
state_mgr_ = state_mgr;
|
||||||
|
screen_width_ = screen_width;
|
||||||
|
screen_height_ = screen_height;
|
||||||
|
|
||||||
|
// Actualizar dimensiones del spatial grid
|
||||||
|
spatial_grid_.updateWorldSize(screen_width, screen_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoidManager::updateScreenSize(int width, int height) {
|
||||||
|
screen_width_ = width;
|
||||||
|
screen_height_ = height;
|
||||||
|
|
||||||
|
// Actualizar dimensiones del spatial grid (FASE 2)
|
||||||
|
spatial_grid_.updateWorldSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoidManager::activateBoids() {
|
||||||
|
boids_active_ = true;
|
||||||
|
|
||||||
|
// Desactivar gravedad al entrar en modo boids
|
||||||
|
scene_mgr_->forceBallsGravityOff();
|
||||||
|
|
||||||
|
// Inicializar velocidades aleatorias para los boids
|
||||||
|
auto& balls = scene_mgr_->getBallsMutable();
|
||||||
|
for (auto& ball : balls) {
|
||||||
|
// Dar velocidad inicial aleatoria si está quieto
|
||||||
|
float vx;
|
||||||
|
float vy;
|
||||||
|
ball->getVelocity(vx, vy);
|
||||||
|
if (vx == 0.0f && vy == 0.0f) {
|
||||||
|
// Velocidad aleatoria entre -60 y +60 px/s (time-based)
|
||||||
|
vx = ((rand() % 200 - 100) / 100.0f) * 60.0f;
|
||||||
|
vy = ((rand() % 200 - 100) / 100.0f) * 60.0f;
|
||||||
|
ball->setVelocity(vx, vy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar notificación (solo si NO estamos en modo demo o logo)
|
||||||
|
if ((state_mgr_ != nullptr) && (ui_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||||
|
ui_mgr_->showNotification("Modo boids");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoidManager::deactivateBoids(bool force_gravity_on) {
|
||||||
|
if (!boids_active_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boids_active_ = false;
|
||||||
|
|
||||||
|
// Activar gravedad al salir (si se especifica)
|
||||||
|
if (force_gravity_on) {
|
||||||
|
scene_mgr_->forceBallsGravityOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar notificación (solo si NO estamos en modo demo o logo)
|
||||||
|
if ((state_mgr_ != nullptr) && (ui_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||||
|
ui_mgr_->showNotification("Modo física");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoidManager::toggleBoidsMode(bool force_gravity_on) {
|
||||||
|
if (boids_active_) {
|
||||||
|
deactivateBoids(force_gravity_on);
|
||||||
|
} else {
|
||||||
|
activateBoids();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoidManager::update(float delta_time) {
|
||||||
|
if (!boids_active_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& balls = scene_mgr_->getBallsMutable();
|
||||||
|
|
||||||
|
// FASE 2: Poblar spatial grid al inicio de cada frame (O(n))
|
||||||
|
spatial_grid_.clear();
|
||||||
|
for (auto& ball : balls) {
|
||||||
|
SDL_FRect pos = ball->getPosition();
|
||||||
|
float center_x = pos.x + (pos.w / 2.0f);
|
||||||
|
float center_y = pos.y + (pos.h / 2.0f);
|
||||||
|
spatial_grid_.insert(ball.get(), center_x, center_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar las tres reglas de Reynolds a cada boid
|
||||||
|
// FASE 2: Ahora usa spatial grid para búsquedas O(1) en lugar de O(n)
|
||||||
|
for (auto& ball : balls) {
|
||||||
|
applySeparation(ball.get(), delta_time);
|
||||||
|
applyAlignment(ball.get(), delta_time);
|
||||||
|
applyCohesion(ball.get(), delta_time);
|
||||||
|
applyBoundaries(ball.get());
|
||||||
|
limitSpeed(ball.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar posiciones con velocidades resultantes (time-based)
|
||||||
|
for (auto& ball : balls) {
|
||||||
|
float vx;
|
||||||
|
float vy;
|
||||||
|
ball->getVelocity(vx, vy);
|
||||||
|
|
||||||
|
SDL_FRect pos = ball->getPosition();
|
||||||
|
pos.x += vx * delta_time; // time-based
|
||||||
|
pos.y += vy * delta_time;
|
||||||
|
|
||||||
|
ball->setPosition(pos.x, pos.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// REGLAS DE REYNOLDS (1987)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void BoidManager::applySeparation(Ball* boid, float delta_time) {
|
||||||
|
// Regla 1: Separación - Evitar colisiones con vecinos cercanos
|
||||||
|
float steer_x = 0.0f;
|
||||||
|
float steer_y = 0.0f;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
SDL_FRect pos = boid->getPosition();
|
||||||
|
float center_x = pos.x + (pos.w / 2.0f);
|
||||||
|
float center_y = pos.y + (pos.h / 2.0f);
|
||||||
|
|
||||||
|
// FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n))
|
||||||
|
auto neighbors = spatial_grid_.queryRadius(center_x, center_y, separation_radius_);
|
||||||
|
|
||||||
|
for (Ball* other : neighbors) {
|
||||||
|
if (other == boid) {
|
||||||
|
continue; // Ignorar a sí mismo
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_FRect other_pos = other->getPosition();
|
||||||
|
float other_x = other_pos.x + (other_pos.w / 2.0f);
|
||||||
|
float other_y = other_pos.y + (other_pos.h / 2.0f);
|
||||||
|
|
||||||
|
float dx = center_x - other_x;
|
||||||
|
float dy = center_y - other_y;
|
||||||
|
float distance = std::sqrt((dx * dx) + (dy * dy));
|
||||||
|
|
||||||
|
if (distance > 0.0f && distance < separation_radius_) {
|
||||||
|
// FASE 1.3: Separación más fuerte cuando más cerca (inversamente proporcional a distancia)
|
||||||
|
// Fuerza proporcional a cercanía: 0% en radio máximo, 100% en colisión
|
||||||
|
float separation_strength = (separation_radius_ - distance) / separation_radius_;
|
||||||
|
steer_x += (dx / distance) * separation_strength;
|
||||||
|
steer_y += (dy / distance) * separation_strength;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
// Promedio
|
||||||
|
steer_x /= count;
|
||||||
|
steer_y /= count;
|
||||||
|
|
||||||
|
// Aplicar fuerza de separación
|
||||||
|
float vx;
|
||||||
|
float vy;
|
||||||
|
boid->getVelocity(vx, vy);
|
||||||
|
vx += steer_x * separation_weight_ * delta_time;
|
||||||
|
vy += steer_y * separation_weight_ * delta_time;
|
||||||
|
boid->setVelocity(vx, vy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoidManager::applyAlignment(Ball* boid, float delta_time) {
|
||||||
|
// Regla 2: Alineación - Seguir dirección promedio del grupo
|
||||||
|
float avg_vx = 0.0f;
|
||||||
|
float avg_vy = 0.0f;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
SDL_FRect pos = boid->getPosition();
|
||||||
|
float center_x = pos.x + (pos.w / 2.0f);
|
||||||
|
float center_y = pos.y + (pos.h / 2.0f);
|
||||||
|
|
||||||
|
// FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n))
|
||||||
|
auto neighbors = spatial_grid_.queryRadius(center_x, center_y, alignment_radius_);
|
||||||
|
|
||||||
|
for (Ball* other : neighbors) {
|
||||||
|
if (other == boid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_FRect other_pos = other->getPosition();
|
||||||
|
float other_x = other_pos.x + (other_pos.w / 2.0f);
|
||||||
|
float other_y = other_pos.y + (other_pos.h / 2.0f);
|
||||||
|
|
||||||
|
float dx = center_x - other_x;
|
||||||
|
float dy = center_y - other_y;
|
||||||
|
float distance = std::sqrt((dx * dx) + (dy * dy));
|
||||||
|
|
||||||
|
if (distance < alignment_radius_) {
|
||||||
|
float other_vx;
|
||||||
|
float other_vy;
|
||||||
|
other->getVelocity(other_vx, other_vy);
|
||||||
|
avg_vx += other_vx;
|
||||||
|
avg_vy += other_vy;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
// Velocidad promedio del grupo
|
||||||
|
avg_vx /= count;
|
||||||
|
avg_vy /= count;
|
||||||
|
|
||||||
|
// Steering hacia la velocidad promedio
|
||||||
|
float vx;
|
||||||
|
float vy;
|
||||||
|
boid->getVelocity(vx, vy);
|
||||||
|
float steer_x = (avg_vx - vx) * alignment_weight_ * delta_time;
|
||||||
|
float steer_y = (avg_vy - vy) * alignment_weight_ * delta_time;
|
||||||
|
|
||||||
|
// Limitar fuerza máxima de steering
|
||||||
|
float steer_mag = std::sqrt((steer_x * steer_x) + (steer_y * steer_y));
|
||||||
|
if (steer_mag > max_force_) {
|
||||||
|
steer_x = (steer_x / steer_mag) * max_force_;
|
||||||
|
steer_y = (steer_y / steer_mag) * max_force_;
|
||||||
|
}
|
||||||
|
|
||||||
|
vx += steer_x;
|
||||||
|
vy += steer_y;
|
||||||
|
boid->setVelocity(vx, vy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoidManager::applyCohesion(Ball* boid, float delta_time) {
|
||||||
|
// Regla 3: Cohesión - Moverse hacia el centro de masa del grupo
|
||||||
|
float center_of_mass_x = 0.0f;
|
||||||
|
float center_of_mass_y = 0.0f;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
SDL_FRect pos = boid->getPosition();
|
||||||
|
float center_x = pos.x + (pos.w / 2.0f);
|
||||||
|
float center_y = pos.y + (pos.h / 2.0f);
|
||||||
|
|
||||||
|
// FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n))
|
||||||
|
auto neighbors = spatial_grid_.queryRadius(center_x, center_y, cohesion_radius_);
|
||||||
|
|
||||||
|
for (Ball* other : neighbors) {
|
||||||
|
if (other == boid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_FRect other_pos = other->getPosition();
|
||||||
|
float other_x = other_pos.x + (other_pos.w / 2.0f);
|
||||||
|
float other_y = other_pos.y + (other_pos.h / 2.0f);
|
||||||
|
|
||||||
|
float dx = center_x - other_x;
|
||||||
|
float dy = center_y - other_y;
|
||||||
|
float distance = std::sqrt((dx * dx) + (dy * dy));
|
||||||
|
|
||||||
|
if (distance < cohesion_radius_) {
|
||||||
|
center_of_mass_x += other_x;
|
||||||
|
center_of_mass_y += other_y;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
// Centro de masa del grupo
|
||||||
|
center_of_mass_x /= count;
|
||||||
|
center_of_mass_y /= count;
|
||||||
|
|
||||||
|
// FASE 1.4: Normalizar dirección hacia el centro (CRÍTICO - antes no estaba normalizado!)
|
||||||
|
float dx_to_center = center_of_mass_x - center_x;
|
||||||
|
float dy_to_center = center_of_mass_y - center_y;
|
||||||
|
float distance_to_center = std::sqrt((dx_to_center * dx_to_center) + (dy_to_center * dy_to_center));
|
||||||
|
|
||||||
|
// Solo aplicar si hay distancia al centro (evitar división por cero)
|
||||||
|
if (distance_to_center > 0.1f) {
|
||||||
|
// Normalizar vector dirección (fuerza independiente de distancia)
|
||||||
|
float steer_x = (dx_to_center / distance_to_center) * cohesion_weight_ * delta_time;
|
||||||
|
float steer_y = (dy_to_center / distance_to_center) * cohesion_weight_ * delta_time;
|
||||||
|
|
||||||
|
// Limitar fuerza máxima de steering
|
||||||
|
float steer_mag = std::sqrt((steer_x * steer_x) + (steer_y * steer_y));
|
||||||
|
if (steer_mag > max_force_) {
|
||||||
|
steer_x = (steer_x / steer_mag) * max_force_;
|
||||||
|
steer_y = (steer_y / steer_mag) * max_force_;
|
||||||
|
}
|
||||||
|
|
||||||
|
float vx;
|
||||||
|
float vy;
|
||||||
|
boid->getVelocity(vx, vy);
|
||||||
|
vx += steer_x;
|
||||||
|
vy += steer_y;
|
||||||
|
boid->setVelocity(vx, vy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoidManager::applyBoundaries(Ball* boid) const {
|
||||||
|
// NUEVA IMPLEMENTACIÓN: Bordes como obstáculos (repulsión en lugar de wrapping)
|
||||||
|
// Cuando un boid se acerca a un borde, se aplica una fuerza alejándolo
|
||||||
|
SDL_FRect pos = boid->getPosition();
|
||||||
|
float center_x = pos.x + (pos.w / 2.0f);
|
||||||
|
float center_y = pos.y + (pos.h / 2.0f);
|
||||||
|
|
||||||
|
float steer_x = 0.0f;
|
||||||
|
float steer_y = 0.0f;
|
||||||
|
|
||||||
|
// Borde izquierdo (x < boundary_margin_)
|
||||||
|
if (center_x < boundary_margin_) {
|
||||||
|
float distance = center_x; // Distancia al borde (0 = colisión)
|
||||||
|
if (distance < boundary_margin_) {
|
||||||
|
// Fuerza proporcional a cercanía: 0% en margen, 100% en colisión
|
||||||
|
float repulsion_strength = (boundary_margin_ - distance) / boundary_margin_;
|
||||||
|
steer_x += repulsion_strength; // Empujar hacia la derecha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Borde derecho (x > screen_width_ - boundary_margin_)
|
||||||
|
if (center_x > screen_width_ - boundary_margin_) {
|
||||||
|
float distance = screen_width_ - center_x;
|
||||||
|
if (distance < boundary_margin_) {
|
||||||
|
float repulsion_strength = (boundary_margin_ - distance) / boundary_margin_;
|
||||||
|
steer_x -= repulsion_strength; // Empujar hacia la izquierda
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Borde superior (y < boundary_margin_)
|
||||||
|
if (center_y < boundary_margin_) {
|
||||||
|
float distance = center_y;
|
||||||
|
if (distance < boundary_margin_) {
|
||||||
|
float repulsion_strength = (boundary_margin_ - distance) / boundary_margin_;
|
||||||
|
steer_y += repulsion_strength; // Empujar hacia abajo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Borde inferior (y > screen_height_ - boundary_margin_)
|
||||||
|
if (center_y > screen_height_ - boundary_margin_) {
|
||||||
|
float distance = screen_height_ - center_y;
|
||||||
|
if (distance < boundary_margin_) {
|
||||||
|
float repulsion_strength = (boundary_margin_ - distance) / boundary_margin_;
|
||||||
|
steer_y -= repulsion_strength; // Empujar hacia arriba
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar fuerza de repulsión si hay alguna
|
||||||
|
if (steer_x != 0.0f || steer_y != 0.0f) {
|
||||||
|
float vx;
|
||||||
|
float vy;
|
||||||
|
boid->getVelocity(vx, vy);
|
||||||
|
|
||||||
|
// Normalizar fuerza de repulsión (para que todas las direcciones tengan la misma intensidad)
|
||||||
|
float steer_mag = std::sqrt((steer_x * steer_x) + (steer_y * steer_y));
|
||||||
|
if (steer_mag > 0.0f) {
|
||||||
|
steer_x /= steer_mag;
|
||||||
|
steer_y /= steer_mag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar aceleración de repulsión (time-based)
|
||||||
|
// boundary_weight_ es más fuerte que separation para garantizar que no escapen
|
||||||
|
vx += steer_x * boundary_weight_ * (1.0f / 60.0f); // Simular delta_time fijo para independencia
|
||||||
|
vy += steer_y * boundary_weight_ * (1.0f / 60.0f);
|
||||||
|
boid->setVelocity(vx, vy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoidManager::limitSpeed(Ball* boid) const {
|
||||||
|
// Limitar velocidad máxima del boid
|
||||||
|
float vx;
|
||||||
|
float vy;
|
||||||
|
boid->getVelocity(vx, vy);
|
||||||
|
|
||||||
|
float speed = std::sqrt((vx * vx) + (vy * vy));
|
||||||
|
|
||||||
|
// Limitar velocidad máxima
|
||||||
|
if (speed > max_speed_) {
|
||||||
|
vx = (vx / speed) * max_speed_;
|
||||||
|
vy = (vy / speed) * max_speed_;
|
||||||
|
boid->setVelocity(vx, vy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FASE 1.2: Aplicar velocidad mínima (evitar boids estáticos)
|
||||||
|
if (speed > 0.0f && speed < min_speed_) {
|
||||||
|
vx = (vx / speed) * min_speed_;
|
||||||
|
vy = (vy / speed) * min_speed_;
|
||||||
|
boid->setVelocity(vx, vy);
|
||||||
|
}
|
||||||
|
}
|
||||||
125
source/boids_mgr/boid_manager.hpp
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef> // for size_t
|
||||||
|
|
||||||
|
#include "defines.hpp" // for SimulationMode, AppMode
|
||||||
|
#include "spatial_grid.hpp" // for SpatialGrid
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
class Engine;
|
||||||
|
class SceneManager;
|
||||||
|
class UIManager;
|
||||||
|
class StateManager;
|
||||||
|
class Ball;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class BoidManager
|
||||||
|
* @brief Gestiona el comportamiento de enjambre (boids)
|
||||||
|
*
|
||||||
|
* Responsabilidad única: Implementación de algoritmo de boids (Reynolds 1987)
|
||||||
|
*
|
||||||
|
* Características:
|
||||||
|
* - Separación: Evitar colisiones con vecinos cercanos
|
||||||
|
* - Alineación: Seguir dirección promedio del grupo
|
||||||
|
* - Cohesión: Moverse hacia el centro de masa del grupo
|
||||||
|
* - Comportamiento emergente sin control centralizado
|
||||||
|
* - Física de steering behavior (velocidad limitada)
|
||||||
|
*/
|
||||||
|
class BoidManager {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Constructor
|
||||||
|
*/
|
||||||
|
BoidManager();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Destructor
|
||||||
|
*/
|
||||||
|
~BoidManager();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Inicializa el BoidManager con referencias a componentes del Engine
|
||||||
|
* @param engine Puntero al Engine (para acceso a recursos)
|
||||||
|
* @param scene_mgr Puntero a SceneManager (acceso a bolas)
|
||||||
|
* @param ui_mgr Puntero a UIManager (notificaciones)
|
||||||
|
* @param state_mgr Puntero a StateManager (estados de aplicación)
|
||||||
|
* @param screen_width Ancho de pantalla actual
|
||||||
|
* @param screen_height Alto de pantalla actual
|
||||||
|
*/
|
||||||
|
void initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, StateManager* state_mgr, int screen_width, int screen_height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Actualiza el tamaño de pantalla (llamado en resize/fullscreen)
|
||||||
|
* @param width Nuevo ancho de pantalla
|
||||||
|
* @param height Nuevo alto de pantalla
|
||||||
|
*/
|
||||||
|
void updateScreenSize(int width, int height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Activa el modo boids
|
||||||
|
*/
|
||||||
|
void activateBoids();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Desactiva el modo boids (vuelve a física normal)
|
||||||
|
* @param force_gravity_on Si debe forzar gravedad ON al salir
|
||||||
|
*/
|
||||||
|
void deactivateBoids(bool force_gravity_on = true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Toggle entre modo boids y modo física
|
||||||
|
* @param force_gravity_on Si debe forzar gravedad ON al salir de boids
|
||||||
|
*/
|
||||||
|
void toggleBoidsMode(bool force_gravity_on = true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Actualiza el comportamiento de todas las bolas como boids
|
||||||
|
* @param delta_time Delta time para física
|
||||||
|
*/
|
||||||
|
void update(float delta_time);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Verifica si el modo boids está activo
|
||||||
|
* @return true si modo boids está activo
|
||||||
|
*/
|
||||||
|
bool isBoidsActive() const { return boids_active_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Referencias a componentes del Engine
|
||||||
|
Engine* engine_;
|
||||||
|
SceneManager* scene_mgr_;
|
||||||
|
UIManager* ui_mgr_;
|
||||||
|
StateManager* state_mgr_;
|
||||||
|
|
||||||
|
// Tamaño de pantalla
|
||||||
|
int screen_width_;
|
||||||
|
int screen_height_;
|
||||||
|
|
||||||
|
// Estado del modo boids
|
||||||
|
bool boids_active_;
|
||||||
|
|
||||||
|
// Spatial Hash Grid para optimización O(n²) → O(n)
|
||||||
|
// FASE 2: Grid reutilizable para búsquedas de vecinos
|
||||||
|
SpatialGrid spatial_grid_;
|
||||||
|
|
||||||
|
// === Parámetros ajustables en runtime (inicializados con valores de defines.h) ===
|
||||||
|
// Permite modificar comportamiento sin recompilar (para tweaking/debug visual)
|
||||||
|
float separation_radius_; // Radio de separación (evitar colisiones)
|
||||||
|
float alignment_radius_; // Radio de alineación (matching de velocidad)
|
||||||
|
float cohesion_radius_; // Radio de cohesión (centro de masa)
|
||||||
|
float separation_weight_; // Peso fuerza de separación (aceleración px/s²)
|
||||||
|
float alignment_weight_; // Peso fuerza de alineación (steering proporcional)
|
||||||
|
float cohesion_weight_; // Peso fuerza de cohesión (aceleración px/s²)
|
||||||
|
float max_speed_; // Velocidad máxima (px/s)
|
||||||
|
float min_speed_; // Velocidad mínima (px/s)
|
||||||
|
float max_force_; // Fuerza máxima de steering (px/s)
|
||||||
|
float boundary_margin_; // Margen para repulsión de bordes (px)
|
||||||
|
float boundary_weight_; // Peso fuerza de repulsión de bordes (aceleración px/s²)
|
||||||
|
|
||||||
|
// Métodos privados para las reglas de Reynolds
|
||||||
|
void applySeparation(Ball* boid, float delta_time);
|
||||||
|
void applyAlignment(Ball* boid, float delta_time);
|
||||||
|
void applyCohesion(Ball* boid, float delta_time);
|
||||||
|
void applyBoundaries(Ball* boid) const; // Repulsión de bordes (ya no wrapping)
|
||||||
|
void limitSpeed(Ball* boid) const; // Limitar velocidad máxima
|
||||||
|
};
|
||||||
93
source/boids_mgr/spatial_grid.cpp
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#include "spatial_grid.hpp"
|
||||||
|
|
||||||
|
#include <algorithm> // for std::max, std::min
|
||||||
|
#include <cmath> // for std::floor, std::ceil
|
||||||
|
|
||||||
|
#include "ball.hpp" // for Ball
|
||||||
|
|
||||||
|
SpatialGrid::SpatialGrid(int world_width, int world_height, float cell_size)
|
||||||
|
: world_width_(world_width),
|
||||||
|
world_height_(world_height),
|
||||||
|
cell_size_(cell_size) {
|
||||||
|
// Calcular número de celdas en cada dimensión
|
||||||
|
grid_cols_ = static_cast<int>(std::ceil(world_width / cell_size));
|
||||||
|
grid_rows_ = static_cast<int>(std::ceil(world_height / cell_size));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpatialGrid::clear() {
|
||||||
|
// Limpiar todos los vectores de celdas (O(n) donde n = número de celdas ocupadas)
|
||||||
|
cells_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpatialGrid::insert(Ball* ball, float x, float y) {
|
||||||
|
// Obtener coordenadas de celda
|
||||||
|
int cell_x;
|
||||||
|
int cell_y;
|
||||||
|
getCellCoords(x, y, cell_x, cell_y);
|
||||||
|
|
||||||
|
// Generar hash key y añadir a la celda
|
||||||
|
int key = getCellKey(cell_x, cell_y);
|
||||||
|
cells_[key].push_back(ball);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SpatialGrid::queryRadius(float x, float y, float radius) -> std::vector<Ball*> {
|
||||||
|
std::vector<Ball*> results;
|
||||||
|
|
||||||
|
// Calcular rango de celdas a revisar (AABB del círculo de búsqueda)
|
||||||
|
int min_cell_x;
|
||||||
|
int min_cell_y;
|
||||||
|
int max_cell_x;
|
||||||
|
int max_cell_y;
|
||||||
|
getCellCoords(x - radius, y - radius, min_cell_x, min_cell_y);
|
||||||
|
getCellCoords(x + radius, y + radius, max_cell_x, max_cell_y);
|
||||||
|
|
||||||
|
// Iterar sobre todas las celdas dentro del AABB
|
||||||
|
for (int cy = min_cell_y; cy <= max_cell_y; cy++) {
|
||||||
|
for (int cx = min_cell_x; cx <= max_cell_x; cx++) {
|
||||||
|
// Verificar que la celda está dentro del grid
|
||||||
|
if (cx < 0 || cx >= grid_cols_ || cy < 0 || cy >= grid_rows_) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener key de la celda
|
||||||
|
int key = getCellKey(cx, cy);
|
||||||
|
|
||||||
|
// Si la celda existe en el mapa, añadir todos sus objetos
|
||||||
|
auto it = cells_.find(key);
|
||||||
|
if (it != cells_.end()) {
|
||||||
|
// Añadir todos los objetos de esta celda al resultado
|
||||||
|
results.insert(results.end(), it->second.begin(), it->second.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpatialGrid::updateWorldSize(int world_width, int world_height) {
|
||||||
|
world_width_ = world_width;
|
||||||
|
world_height_ = world_height;
|
||||||
|
|
||||||
|
// Recalcular dimensiones del grid
|
||||||
|
grid_cols_ = static_cast<int>(std::ceil(world_width / cell_size_));
|
||||||
|
grid_rows_ = static_cast<int>(std::ceil(world_height / cell_size_));
|
||||||
|
|
||||||
|
// Limpiar grid (las posiciones anteriores ya no son válidas)
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MÉTODOS PRIVADOS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void SpatialGrid::getCellCoords(float x, float y, int& cell_x, int& cell_y) const {
|
||||||
|
// Convertir coordenadas del mundo a coordenadas de celda
|
||||||
|
cell_x = static_cast<int>(std::floor(x / cell_size_));
|
||||||
|
cell_y = static_cast<int>(std::floor(y / cell_size_));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SpatialGrid::getCellKey(int cell_x, int cell_y) const -> int {
|
||||||
|
// Hash espacial 2D → 1D usando codificación por filas
|
||||||
|
// Formula: key = y * ancho + x (similar a array 2D aplanado)
|
||||||
|
return (cell_y * grid_cols_) + cell_x;
|
||||||
|
}
|
||||||
71
source/boids_mgr/spatial_grid.hpp
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class Ball; // Forward declaration
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SPATIAL HASH GRID - Sistema genérico de particionamiento espacial
|
||||||
|
// ============================================================================
|
||||||
|
//
|
||||||
|
// Divide el espacio 2D en celdas de tamaño fijo para acelerar búsquedas de vecinos.
|
||||||
|
// Reduce complejidad de O(n²) a O(n) para queries de proximidad.
|
||||||
|
//
|
||||||
|
// CASOS DE USO:
|
||||||
|
// - Boids: Buscar vecinos para reglas de Reynolds (separación/alineación/cohesión)
|
||||||
|
// - Física: Detección de colisiones ball-to-ball (futuro)
|
||||||
|
// - IA: Pathfinding con obstáculos dinámicos
|
||||||
|
//
|
||||||
|
// ALGORITMO:
|
||||||
|
// 1. Dividir pantalla en grid de celdas (ej: 100x100px cada una)
|
||||||
|
// 2. Insertar cada Ball en celda(s) correspondiente(s) según posición
|
||||||
|
// 3. Query: Solo revisar celdas adyacentes (9 celdas max) en lugar de TODOS los objetos
|
||||||
|
//
|
||||||
|
// MEJORA DE RENDIMIENTO:
|
||||||
|
// - Sin grid: 1000 boids = 1M comparaciones (1000²)
|
||||||
|
// - Con grid: 1000 boids ≈ 9K comparaciones (1000 * ~9 vecinos/celda promedio)
|
||||||
|
// - Speedup: ~100x en casos típicos
|
||||||
|
//
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
class SpatialGrid {
|
||||||
|
public:
|
||||||
|
// Constructor: especificar dimensiones del mundo y tamaño de celda
|
||||||
|
SpatialGrid(int world_width, int world_height, float cell_size);
|
||||||
|
|
||||||
|
// Limpiar todas las celdas (llamar al inicio de cada frame)
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
// Insertar objeto en el grid según su posición (x, y)
|
||||||
|
void insert(Ball* ball, float x, float y);
|
||||||
|
|
||||||
|
// Buscar todos los objetos dentro del radio especificado desde (x, y)
|
||||||
|
// Devuelve vector de punteros a Ball (puede contener duplicados si ball está en múltiples celdas)
|
||||||
|
std::vector<Ball*> queryRadius(float x, float y, float radius);
|
||||||
|
|
||||||
|
// Actualizar dimensiones del mundo (útil para cambios de resolución F4)
|
||||||
|
void updateWorldSize(int world_width, int world_height);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Convertir coordenadas (x, y) a índice de celda (cell_x, cell_y)
|
||||||
|
void getCellCoords(float x, float y, int& cell_x, int& cell_y) const;
|
||||||
|
|
||||||
|
// Convertir (cell_x, cell_y) a hash key único para el mapa
|
||||||
|
int getCellKey(int cell_x, int cell_y) const;
|
||||||
|
|
||||||
|
// Dimensiones del mundo (ancho/alto en píxeles)
|
||||||
|
int world_width_;
|
||||||
|
int world_height_;
|
||||||
|
|
||||||
|
// Tamaño de cada celda (en píxeles)
|
||||||
|
float cell_size_;
|
||||||
|
|
||||||
|
// Número de celdas en cada dimensión
|
||||||
|
int grid_cols_;
|
||||||
|
int grid_rows_;
|
||||||
|
|
||||||
|
// Estructura de datos: hash map de cell_key → vector de Ball*
|
||||||
|
// Usamos unordered_map para O(1) lookup
|
||||||
|
std::unordered_map<int, std::vector<Ball*>> cells_;
|
||||||
|
};
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
// Configuración de ventana y pantalla
|
|
||||||
constexpr char WINDOW_CAPTION[] = "vibe3_physics";
|
|
||||||
|
|
||||||
constexpr int SCREEN_WIDTH = 320; // Ancho de la pantalla lógica (píxeles)
|
|
||||||
constexpr int SCREEN_HEIGHT = 240; // Alto de la pantalla lógica (píxeles)
|
|
||||||
constexpr int WINDOW_ZOOM = 3; // Zoom inicial de la ventana
|
|
||||||
constexpr int BALL_SIZE = 10; // Tamaño de las pelotas (píxeles)
|
|
||||||
|
|
||||||
// Configuración de zoom dinámico de ventana
|
|
||||||
constexpr int WINDOW_ZOOM_MIN = 1; // Zoom mínimo (320x240)
|
|
||||||
constexpr int WINDOW_ZOOM_MAX = 10; // Zoom máximo teórico (3200x2400)
|
|
||||||
constexpr int WINDOW_DESKTOP_MARGIN = 10; // Margen mínimo con bordes del escritorio
|
|
||||||
constexpr int WINDOW_DECORATION_HEIGHT = 30; // Altura estimada de decoraciones del SO
|
|
||||||
|
|
||||||
// Configuración de física
|
|
||||||
constexpr float GRAVITY_FORCE = 0.2f; // Fuerza de gravedad (píxeles/frame²)
|
|
||||||
|
|
||||||
// Configuración de interfaz
|
|
||||||
constexpr Uint64 TEXT_DURATION = 2000; // Duración del texto informativo (ms)
|
|
||||||
|
|
||||||
// Configuración de pérdida aleatoria en rebotes
|
|
||||||
constexpr float BASE_BOUNCE_COEFFICIENT = 0.75f; // Coeficiente base IGUAL para todas las pelotas
|
|
||||||
constexpr float BOUNCE_RANDOM_LOSS_PERCENT = 0.1f; // 0-10% pérdida adicional aleatoria en cada rebote
|
|
||||||
constexpr float LATERAL_LOSS_PERCENT = 0.02f; // ±2% pérdida lateral en rebotes
|
|
||||||
|
|
||||||
// Configuración de masa/peso individual por pelota
|
|
||||||
constexpr float GRAVITY_MASS_MIN = 0.7f; // Factor mínimo de masa (pelota ligera - 70% gravedad)
|
|
||||||
constexpr float GRAVITY_MASS_MAX = 1.3f; // Factor máximo de masa (pelota pesada - 130% gravedad)
|
|
||||||
|
|
||||||
// Configuración de velocidad lateral al cambiar gravedad (muy sutil)
|
|
||||||
constexpr float GRAVITY_CHANGE_LATERAL_MIN = 0.04f; // Velocidad lateral mínima (2.4 px/s)
|
|
||||||
constexpr float GRAVITY_CHANGE_LATERAL_MAX = 0.08f; // Velocidad lateral máxima (4.8 px/s)
|
|
||||||
|
|
||||||
// Configuración de spawn inicial de pelotas
|
|
||||||
constexpr float BALL_SPAWN_MARGIN = 0.15f; // Margen lateral para spawn (0.25 = 25% a cada lado)
|
|
||||||
|
|
||||||
// Estructura para representar colores RGB
|
|
||||||
struct Color {
|
|
||||||
int r, g, b; // Componentes rojo, verde, azul (0-255)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Enum para dirección de gravedad
|
|
||||||
enum class GravityDirection {
|
|
||||||
DOWN, // ↓ Gravedad hacia abajo (por defecto)
|
|
||||||
UP, // ↑ Gravedad hacia arriba
|
|
||||||
LEFT, // ← Gravedad hacia la izquierda
|
|
||||||
RIGHT // → Gravedad hacia la derecha
|
|
||||||
};
|
|
||||||
|
|
||||||
// Enum para temas de colores (seleccionables con teclado numérico)
|
|
||||||
enum class ColorTheme {
|
|
||||||
SUNSET = 0, // Naranjas, rojos, amarillos, rosas
|
|
||||||
OCEAN = 1, // Azules, turquesas, blancos
|
|
||||||
NEON = 2, // Cian, magenta, verde lima, amarillo vibrante
|
|
||||||
FOREST = 3, // Verdes, marrones, amarillos otoño
|
|
||||||
RGB = 4 // RGB puros y subdivisiones matemáticas (fondo blanco)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Enum para modo de simulación
|
|
||||||
enum class SimulationMode {
|
|
||||||
PHYSICS, // Modo física normal con gravedad
|
|
||||||
ROTOBALL // Modo esfera 3D rotante (demoscene effect)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Configuración de RotoBall (esfera 3D rotante)
|
|
||||||
constexpr float ROTOBALL_RADIUS = 80.0f; // Radio de la esfera (píxeles)
|
|
||||||
constexpr float ROTOBALL_ROTATION_SPEED_Y = 1.5f; // Velocidad rotación eje Y (rad/s)
|
|
||||||
constexpr float ROTOBALL_ROTATION_SPEED_X = 0.8f; // Velocidad rotación eje X (rad/s)
|
|
||||||
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 = 35.0f; // Amortiguación base (amortiguamiento crítico ≈ 2*√k*m)
|
|
||||||
constexpr float ROTOBALL_DAMPING_NEAR = 80.0f; // Amortiguación cerca del punto (absorción rápida)
|
|
||||||
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
|
|
||||||
401
source/defines.hpp
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_stdinc.h> // for Uint64
|
||||||
|
|
||||||
|
#include <vector> // for std::vector in DynamicThemeKeyframe/DynamicTheme
|
||||||
|
|
||||||
|
// Configuración de ventana y pantalla
|
||||||
|
constexpr char WINDOW_CAPTION[] = "© 2025 ViBe3 Physics — JailDesigner";
|
||||||
|
|
||||||
|
// Resolución por defecto (usada si no se especifica en CLI)
|
||||||
|
constexpr int DEFAULT_SCREEN_WIDTH = 1280; // Ancho lógico por defecto (si no hay -w)
|
||||||
|
constexpr int DEFAULT_SCREEN_HEIGHT = 720; // Alto lógico por defecto (si no hay -h)
|
||||||
|
constexpr int DEFAULT_WINDOW_ZOOM = 1; // Zoom inicial de ventana (1x = sin zoom)
|
||||||
|
|
||||||
|
constexpr int WINDOW_DESKTOP_MARGIN = 10; // Margen mínimo con bordes del escritorio
|
||||||
|
constexpr int WINDOW_DECORATION_HEIGHT = 30; // Altura estimada de decoraciones del SO
|
||||||
|
|
||||||
|
// Configuración de escala de ventana por pasos (F1/F2)
|
||||||
|
constexpr float WINDOW_SCALE_STEP = 0.1f; // Incremento/decremento por pulsación (10%)
|
||||||
|
constexpr float WINDOW_SCALE_MIN = 0.5f; // Escala mínima (50% de la resolución base)
|
||||||
|
|
||||||
|
// Configuración de física
|
||||||
|
constexpr float GRAVITY_FORCE = 0.2f; // Fuerza de gravedad (píxeles/frame²)
|
||||||
|
|
||||||
|
// Fuente de la interfaz
|
||||||
|
#define APP_FONT "data/fonts/Exo2-Regular.ttf"
|
||||||
|
|
||||||
|
// Configuración de interfaz
|
||||||
|
constexpr float THEME_TRANSITION_DURATION = 0.5f; // Duración de transiciones LERP entre temas (segundos)
|
||||||
|
|
||||||
|
// Configuración de notificaciones (sistema Notifier)
|
||||||
|
constexpr int TEXT_ABSOLUTE_SIZE = 12; // Tamaño fuente base en píxeles físicos (múltiplo de 12px, tamaño nativo de la fuente)
|
||||||
|
constexpr Uint64 NOTIFICATION_DURATION = 2000; // Duración default de notificaciones (ms)
|
||||||
|
constexpr Uint64 NOTIFICATION_SLIDE_TIME = 300; // Duración animación entrada (ms)
|
||||||
|
constexpr Uint64 NOTIFICATION_FADE_TIME = 200; // Duración animación salida (ms)
|
||||||
|
constexpr float NOTIFICATION_BG_ALPHA = 0.7f; // Opacidad fondo semitransparente (0.0-1.0)
|
||||||
|
constexpr int NOTIFICATION_PADDING = 10; // Padding interno del fondo (píxeles físicos)
|
||||||
|
constexpr int NOTIFICATION_TOP_MARGIN = 20; // Margen superior desde borde pantalla (píxeles físicos)
|
||||||
|
constexpr char KIOSK_NOTIFICATION_TEXT[] = "Modo kiosko";
|
||||||
|
|
||||||
|
// Configuración de pérdida aleatoria en rebotes
|
||||||
|
constexpr float BASE_BOUNCE_COEFFICIENT = 0.75f; // Coeficiente base IGUAL para todas las pelotas
|
||||||
|
constexpr float BOUNCE_RANDOM_LOSS_PERCENT = 0.1f; // 0-10% pérdida adicional aleatoria en cada rebote
|
||||||
|
constexpr float LATERAL_LOSS_PERCENT = 0.02f; // ±2% pérdida lateral en rebotes
|
||||||
|
|
||||||
|
// Configuración de masa/peso individual por pelota
|
||||||
|
constexpr float GRAVITY_MASS_MIN = 0.7f; // Factor mínimo de masa (pelota ligera - 70% gravedad)
|
||||||
|
constexpr float GRAVITY_MASS_MAX = 1.3f; // Factor máximo de masa (pelota pesada - 130% gravedad)
|
||||||
|
|
||||||
|
// Configuración de velocidad lateral al cambiar gravedad (muy sutil)
|
||||||
|
constexpr float GRAVITY_CHANGE_LATERAL_MIN = 0.04f; // Velocidad lateral mínima (2.4 px/s)
|
||||||
|
constexpr float GRAVITY_CHANGE_LATERAL_MAX = 0.08f; // Velocidad lateral máxima (4.8 px/s)
|
||||||
|
|
||||||
|
// Configuración de spawn inicial de pelotas
|
||||||
|
constexpr float BALL_SPAWN_MARGIN = 0.15f; // Margen lateral para spawn (0.25 = 25% a cada lado)
|
||||||
|
|
||||||
|
// Escenarios de número de pelotas (teclas 1-8)
|
||||||
|
constexpr int SCENE_BALLS_1 = 10;
|
||||||
|
constexpr int SCENE_BALLS_2 = 50;
|
||||||
|
constexpr int SCENE_BALLS_3 = 100;
|
||||||
|
constexpr int SCENE_BALLS_4 = 500;
|
||||||
|
constexpr int SCENE_BALLS_5 = 1000;
|
||||||
|
constexpr int SCENE_BALLS_6 = 5000;
|
||||||
|
constexpr int SCENE_BALLS_7 = 10000;
|
||||||
|
constexpr int SCENE_BALLS_8 = 50000; // Máximo escenario estándar (tecla 8)
|
||||||
|
|
||||||
|
constexpr int SCENARIO_COUNT = 8;
|
||||||
|
constexpr int BALL_COUNT_SCENARIOS[SCENARIO_COUNT] = {
|
||||||
|
SCENE_BALLS_1,
|
||||||
|
SCENE_BALLS_2,
|
||||||
|
SCENE_BALLS_3,
|
||||||
|
SCENE_BALLS_4,
|
||||||
|
SCENE_BALLS_5,
|
||||||
|
SCENE_BALLS_6,
|
||||||
|
SCENE_BALLS_7,
|
||||||
|
SCENE_BALLS_8};
|
||||||
|
|
||||||
|
constexpr int BOIDS_MAX_BALLS = SCENE_BALLS_5; // 1 000 bolas máximo en modo BOIDS
|
||||||
|
constexpr int DEMO_AUTO_MIN_SCENARIO = 2; // mínimo 100 bolas
|
||||||
|
constexpr int DEMO_AUTO_MAX_SCENARIO = 7; // máximo sin restricción hardware (ajustado por benchmark)
|
||||||
|
constexpr int LOGO_MIN_SCENARIO_IDX = 4; // mínimo 1000 bolas (sustituye LOGO_MODE_MIN_BALLS)
|
||||||
|
constexpr int CUSTOM_SCENARIO_IDX = 8; // Escenario custom opcional (tecla 9, --custom-balls)
|
||||||
|
|
||||||
|
// Estructura para representar colores RGB
|
||||||
|
struct Color {
|
||||||
|
int r, g, b; // Componentes rojo, verde, azul (0-255)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estructura de tema de colores estático
|
||||||
|
struct ThemeColors {
|
||||||
|
const char* name_en; // Nombre en inglés (para debug)
|
||||||
|
const char* name_es; // Nombre en español (para display)
|
||||||
|
int text_color_r, text_color_g, text_color_b; // Color del texto del tema
|
||||||
|
float bg_top_r, bg_top_g, bg_top_b;
|
||||||
|
float bg_bottom_r, bg_bottom_g, bg_bottom_b;
|
||||||
|
std::vector<Color> ball_colors;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estructura para keyframe de tema dinámico
|
||||||
|
struct DynamicThemeKeyframe {
|
||||||
|
// Fondo degradado
|
||||||
|
float bg_top_r, bg_top_g, bg_top_b;
|
||||||
|
float bg_bottom_r, bg_bottom_g, bg_bottom_b;
|
||||||
|
|
||||||
|
// Color de fondo de notificaciones
|
||||||
|
int notif_bg_r, notif_bg_g, notif_bg_b;
|
||||||
|
|
||||||
|
// Colores de pelotas en este keyframe
|
||||||
|
std::vector<Color> ball_colors;
|
||||||
|
|
||||||
|
// Duración de transición HACIA este keyframe (segundos)
|
||||||
|
// 0.0 = estado inicial (sin transición)
|
||||||
|
float duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
// NOTA: La clase DynamicTheme (tema dinámico animado) está definida en themes/dynamic_theme.h
|
||||||
|
// Esta estructura de datos es solo para definir keyframes que se pasan al constructor
|
||||||
|
|
||||||
|
// Enum para dirección de gravedad
|
||||||
|
enum class GravityDirection {
|
||||||
|
DOWN, // ↓ Gravedad hacia abajo (por defecto)
|
||||||
|
UP, // ↑ Gravedad hacia arriba
|
||||||
|
LEFT, // ← Gravedad hacia la izquierda
|
||||||
|
RIGHT // → Gravedad hacia la derecha
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enum para temas de colores (seleccionables con teclado numérico y Shift+Numpad)
|
||||||
|
// Todos los temas usan ahora sistema dinámico de keyframes
|
||||||
|
enum class ColorTheme {
|
||||||
|
SUNSET = 0, // Naranjas, rojos, amarillos, rosas (estático: 1 keyframe)
|
||||||
|
OCEAN = 1, // Azules, turquesas, blancos (estático: 1 keyframe)
|
||||||
|
NEON = 2, // Cian, magenta, verde lima, amarillo vibrante (estático: 1 keyframe)
|
||||||
|
FOREST = 3, // Verdes, marrones, amarillos otoño (estático: 1 keyframe)
|
||||||
|
RGB = 4, // RGB puros y subdivisiones matemáticas - fondo blanco (estático: 1 keyframe)
|
||||||
|
MONOCHROME = 5, // Fondo negro degradado, sprites blancos monocromáticos (estático: 1 keyframe)
|
||||||
|
LAVENDER = 6, // Degradado violeta-azul, pelotas amarillo dorado (estático: 1 keyframe)
|
||||||
|
CRIMSON = 7, // Fondo negro-rojo, pelotas rojas uniformes (estático: 1 keyframe)
|
||||||
|
EMERALD = 8, // Fondo negro-verde, pelotas verdes uniformes (estático: 1 keyframe)
|
||||||
|
SUNRISE = 9, // Amanecer: Noche → Alba → Día (animado: 4 keyframes, 12s ciclo)
|
||||||
|
OCEAN_WAVES = 10, // Olas oceánicas: Azul oscuro ↔ Turquesa (animado: 3 keyframes, 8s ciclo)
|
||||||
|
NEON_PULSE = 11, // Pulso neón: Negro ↔ Neón vibrante (animado: 3 keyframes, 3s ping-pong)
|
||||||
|
FIRE = 12, // Fuego vivo: Brasas → Llamas → Inferno (animado: 4 keyframes, 10s ciclo)
|
||||||
|
AURORA = 13, // Aurora boreal: Verde → Violeta → Cian (animado: 4 keyframes, 14s ciclo)
|
||||||
|
VOLCANIC = 14 // Erupción volcánica: Ceniza → Erupción → Lava (animado: 4 keyframes, 12s ciclo)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enum para tipo de figura 3D
|
||||||
|
enum class ShapeType {
|
||||||
|
NONE, // Sin figura (modo física pura)
|
||||||
|
SPHERE, // Esfera Fibonacci (antiguo RotoBall)
|
||||||
|
CUBE, // Cubo rotante
|
||||||
|
HELIX, // Espiral 3D
|
||||||
|
TORUS, // Toroide/donut
|
||||||
|
LISSAJOUS, // Malla ondeante
|
||||||
|
CYLINDER, // Cilindro rotante
|
||||||
|
ICOSAHEDRON, // Icosaedro D20
|
||||||
|
ATOM, // Átomo con órbitas
|
||||||
|
PNG_SHAPE // Forma cargada desde PNG 1-bit
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enum para modo de simulación
|
||||||
|
enum class SimulationMode {
|
||||||
|
PHYSICS, // Modo física normal con gravedad
|
||||||
|
SHAPE, // Modo figura 3D (Shape polimórfico)
|
||||||
|
BOIDS // Modo enjambre (Boids - comportamiento emergente)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enum para modo de aplicación (mutuamente excluyentes)
|
||||||
|
enum class AppMode {
|
||||||
|
SANDBOX, // Control manual del usuario (modo sandbox)
|
||||||
|
DEMO, // Modo demo completo (auto-play)
|
||||||
|
DEMO_LITE, // Modo demo lite (solo física/figuras)
|
||||||
|
LOGO // Modo logo (easter egg)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enum para modo de escalado en fullscreen (F5)
|
||||||
|
enum class ScalingMode {
|
||||||
|
INTEGER, // Escalado entero con barras negras (mantiene aspecto + píxel perfecto)
|
||||||
|
LETTERBOX, // Zoom hasta llenar una dimensión (una barra desaparece)
|
||||||
|
STRETCH // Estirar para llenar pantalla completa (puede distorsionar aspecto)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configuración de RotoBall (esfera 3D rotante)
|
||||||
|
constexpr float ROTOBALL_RADIUS_FACTOR = 0.333f; // Radio como proporción de altura de pantalla (80/240 ≈ 0.333)
|
||||||
|
constexpr float ROTOBALL_ROTATION_SPEED_Y = 1.5f; // Velocidad rotación eje Y (rad/s)
|
||||||
|
constexpr float ROTOBALL_ROTATION_SPEED_X = 0.8f; // Velocidad rotación eje X (rad/s)
|
||||||
|
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 para figuras 3D (sistema de resorte)
|
||||||
|
// SHAPE: Figuras 3D normales (Q/W/E/R/T/Y/U/I/O) - Mayor pegajosidad
|
||||||
|
constexpr float SHAPE_SPRING_K = 800.0f; // Rigidez alta (pelotas más "pegadas")
|
||||||
|
constexpr float SHAPE_DAMPING_BASE = 60.0f; // Amortiguación alta (menos rebote)
|
||||||
|
constexpr float SHAPE_DAMPING_NEAR = 150.0f; // Absorción muy rápida al llegar
|
||||||
|
constexpr float SHAPE_NEAR_THRESHOLD = 8.0f; // Umbral "cerca" más amplio
|
||||||
|
constexpr float SHAPE_MAX_FORCE = 2000.0f; // Permite fuerzas más fuertes
|
||||||
|
|
||||||
|
// Configuración del Cubo (cubo 3D rotante)
|
||||||
|
constexpr float CUBE_SIZE_FACTOR = 0.25f; // Tamaño como proporción de altura (60/240 = 0.25)
|
||||||
|
constexpr float CUBE_ROTATION_SPEED_X = 0.5f; // Velocidad rotación eje X (rad/s)
|
||||||
|
constexpr float CUBE_ROTATION_SPEED_Y = 0.7f; // Velocidad rotación eje Y (rad/s)
|
||||||
|
constexpr float CUBE_ROTATION_SPEED_Z = 0.3f; // Velocidad rotación eje Z (rad/s)
|
||||||
|
|
||||||
|
// Configuración de Helix (espiral helicoidal 3D)
|
||||||
|
constexpr float HELIX_RADIUS_FACTOR = 0.25f; // Radio de la espiral (proporción de altura)
|
||||||
|
constexpr float HELIX_PITCH_FACTOR = 0.25f; // Separación vertical entre vueltas (proporción de altura)
|
||||||
|
constexpr float HELIX_NUM_TURNS = 3.0f; // Número de vueltas completas (1-5)
|
||||||
|
constexpr float HELIX_ROTATION_SPEED_Y = 1.2f; // Velocidad rotación eje Y (rad/s)
|
||||||
|
constexpr float HELIX_PHASE_SPEED = 0.5f; // Velocidad de animación vertical (rad/s)
|
||||||
|
|
||||||
|
// Configuración de Lissajous Curve 3D (curva paramétrica)
|
||||||
|
constexpr float LISSAJOUS_SIZE_FACTOR = 0.35f; // Amplitud de la curva (proporción de altura)
|
||||||
|
constexpr float LISSAJOUS_FREQ_X = 3.0f; // Frecuencia en eje X (ratio 3:2:1)
|
||||||
|
constexpr float LISSAJOUS_FREQ_Y = 2.0f; // Frecuencia en eje Y
|
||||||
|
constexpr float LISSAJOUS_FREQ_Z = 1.0f; // Frecuencia en eje Z
|
||||||
|
constexpr float LISSAJOUS_PHASE_SPEED = 1.0f; // Velocidad de animación de fase (rad/s)
|
||||||
|
constexpr float LISSAJOUS_ROTATION_SPEED_X = 0.4f; // Velocidad rotación global X (rad/s)
|
||||||
|
constexpr float LISSAJOUS_ROTATION_SPEED_Y = 0.6f; // Velocidad rotación global Y (rad/s)
|
||||||
|
|
||||||
|
// Configuración de Torus (toroide/donut 3D)
|
||||||
|
constexpr float TORUS_MAJOR_RADIUS_FACTOR = 0.25f; // Radio mayor R (centro torus a centro tubo)
|
||||||
|
constexpr float TORUS_MINOR_RADIUS_FACTOR = 0.12f; // Radio menor r (grosor del tubo)
|
||||||
|
constexpr float TORUS_ROTATION_SPEED_X = 0.6f; // Velocidad rotación eje X (rad/s)
|
||||||
|
constexpr float TORUS_ROTATION_SPEED_Y = 0.9f; // Velocidad rotación eje Y (rad/s)
|
||||||
|
constexpr float TORUS_ROTATION_SPEED_Z = 0.3f; // Velocidad rotación eje Z (rad/s)
|
||||||
|
|
||||||
|
// Configuración de Cylinder (cilindro 3D)
|
||||||
|
constexpr float CYLINDER_RADIUS_FACTOR = 0.25f; // Radio del cilindro (proporción de altura)
|
||||||
|
constexpr float CYLINDER_HEIGHT_FACTOR = 0.5f; // Altura del cilindro (proporción de altura)
|
||||||
|
constexpr float CYLINDER_ROTATION_SPEED_Y = 1.0f; // Velocidad rotación eje Y (rad/s)
|
||||||
|
|
||||||
|
// Configuración de Icosahedron (icosaedro D20)
|
||||||
|
constexpr float ICOSAHEDRON_RADIUS_FACTOR = 0.30f; // Radio de la esfera circunscrita
|
||||||
|
constexpr float ICOSAHEDRON_ROTATION_SPEED_X = 0.4f; // Velocidad rotación eje X (rad/s)
|
||||||
|
constexpr float ICOSAHEDRON_ROTATION_SPEED_Y = 0.7f; // Velocidad rotación eje Y (rad/s)
|
||||||
|
constexpr float ICOSAHEDRON_ROTATION_SPEED_Z = 0.2f; // Velocidad rotación eje Z (rad/s)
|
||||||
|
|
||||||
|
// Configuración de Atom (núcleo con órbitas electrónicas)
|
||||||
|
constexpr float ATOM_NUCLEUS_RADIUS_FACTOR = 0.08f; // Radio del núcleo central
|
||||||
|
constexpr float ATOM_ORBIT_RADIUS_FACTOR = 0.30f; // Radio de las órbitas
|
||||||
|
constexpr float ATOM_NUM_ORBITS = 3; // Número de órbitas
|
||||||
|
constexpr float ATOM_ORBIT_ROTATION_SPEED = 2.0f; // Velocidad de electrones (rad/s)
|
||||||
|
constexpr float ATOM_ROTATION_SPEED_Y = 0.5f; // Velocidad rotación global (rad/s)
|
||||||
|
|
||||||
|
// Configuración de PNG Shape (forma desde imagen PNG 1-bit)
|
||||||
|
constexpr float PNG_SIZE_FACTOR = 0.8f; // Tamaño como proporción de altura (80% pantalla)
|
||||||
|
constexpr float PNG_EXTRUSION_DEPTH_FACTOR = 0.12f; // Profundidad de extrusión (compacta)
|
||||||
|
constexpr int PNG_NUM_EXTRUSION_LAYERS = 15; // Capas de extrusión (más capas = más pegajosidad)
|
||||||
|
constexpr bool PNG_USE_EDGES_ONLY = false; // true = solo bordes, false = relleno completo
|
||||||
|
// Rotación "legible" (texto de frente con volteretas ocasionales)
|
||||||
|
constexpr float PNG_IDLE_TIME_MIN = 0.5f; // Tiempo mínimo de frente (segundos) - modo MANUAL
|
||||||
|
constexpr float PNG_IDLE_TIME_MAX = 2.0f; // Tiempo máximo de frente (segundos) - modo MANUAL
|
||||||
|
constexpr float PNG_IDLE_TIME_MIN_LOGO = 2.0f; // Tiempo mínimo de frente en LOGO MODE
|
||||||
|
constexpr float PNG_IDLE_TIME_MAX_LOGO = 4.0f; // Tiempo máximo de frente en LOGO MODE
|
||||||
|
constexpr float PNG_FLIP_SPEED = 3.0f; // Velocidad voltereta (rad/s)
|
||||||
|
constexpr float PNG_FLIP_DURATION = 1.5f; // Duración voltereta (segundos)
|
||||||
|
|
||||||
|
// Control manual de escala de figuras 3D (Numpad +/-)
|
||||||
|
constexpr float SHAPE_SCALE_MIN = 0.3f; // Escala mínima (30%)
|
||||||
|
constexpr float SHAPE_SCALE_MAX = 3.0f; // Escala máxima (300%)
|
||||||
|
constexpr float SHAPE_SCALE_STEP = 0.1f; // Incremento por pulsación
|
||||||
|
constexpr float SHAPE_SCALE_DEFAULT = 1.0f; // Escala por defecto (100%)
|
||||||
|
|
||||||
|
// Configuración de Modo DEMO (auto-play completo)
|
||||||
|
constexpr float DEMO_ACTION_INTERVAL_MIN = 2.0f; // Tiempo mínimo entre acciones (segundos)
|
||||||
|
constexpr float DEMO_ACTION_INTERVAL_MAX = 6.0f; // Tiempo máximo entre acciones (segundos)
|
||||||
|
|
||||||
|
// Pesos de probabilidad DEMO MODE (valores relativos, se normalizan)
|
||||||
|
constexpr int DEMO_WEIGHT_GRAVITY_DIR = 12; // Cambiar dirección gravedad (12%)
|
||||||
|
constexpr int DEMO_WEIGHT_GRAVITY_TOGGLE = 15; // Toggle gravedad ON/OFF (15%) - ¡Ver caer pelotas!
|
||||||
|
constexpr int DEMO_WEIGHT_SHAPE = 22; // Activar figura 3D (22%) - Construir figuras
|
||||||
|
constexpr int DEMO_WEIGHT_TOGGLE_PHYSICS = 18; // Toggle física ↔ figura (18%) - ¡Destruir figuras!
|
||||||
|
constexpr int DEMO_WEIGHT_REGENERATE_SHAPE = 10; // Re-generar misma figura (10%) - Reconstruir
|
||||||
|
constexpr int DEMO_WEIGHT_THEME = 12; // Cambiar tema de colores (12%)
|
||||||
|
constexpr int DEMO_WEIGHT_SCENARIO = 2; // Cambiar número de pelotas (2%) - MUY OCASIONAL
|
||||||
|
constexpr int DEMO_WEIGHT_IMPULSE = 6; // Aplicar impulso (SPACE) (6%)
|
||||||
|
constexpr int DEMO_WEIGHT_DEPTH_ZOOM = 1; // Toggle profundidad (1%)
|
||||||
|
constexpr int DEMO_WEIGHT_SHAPE_SCALE = 1; // Cambiar escala figura (1%)
|
||||||
|
constexpr int DEMO_WEIGHT_SPRITE = 1; // Cambiar sprite (1%)
|
||||||
|
// TOTAL: 100
|
||||||
|
|
||||||
|
// Configuración de Modo DEMO LITE (solo física/figuras)
|
||||||
|
constexpr float DEMO_LITE_ACTION_INTERVAL_MIN = 1.5f; // Más rápido que demo completo
|
||||||
|
constexpr float DEMO_LITE_ACTION_INTERVAL_MAX = 4.0f;
|
||||||
|
|
||||||
|
// Pesos de probabilidad DEMO LITE (solo física/figuras, sin cambios de escenario/tema)
|
||||||
|
constexpr int DEMO_LITE_WEIGHT_GRAVITY_DIR = 25; // Cambiar dirección gravedad (25%)
|
||||||
|
constexpr int DEMO_LITE_WEIGHT_GRAVITY_TOGGLE = 20; // Toggle gravedad ON/OFF (20%)
|
||||||
|
constexpr int DEMO_LITE_WEIGHT_SHAPE = 25; // Activar figura 3D (25%)
|
||||||
|
constexpr int DEMO_LITE_WEIGHT_TOGGLE_PHYSICS = 20; // Toggle física ↔ figura (20%)
|
||||||
|
constexpr int DEMO_LITE_WEIGHT_IMPULSE = 10; // Aplicar impulso (10%)
|
||||||
|
// TOTAL: 100
|
||||||
|
|
||||||
|
// Configuración de Modo LOGO (easter egg - "marca de agua")
|
||||||
|
constexpr float LOGO_MODE_SHAPE_SCALE = 1.2f; // Escala de figura en modo logo (120%)
|
||||||
|
constexpr float LOGO_ACTION_INTERVAL_MIN = 3.0f; // Tiempo mínimo entre alternancia SHAPE/PHYSICS (escalado con resolución)
|
||||||
|
constexpr float LOGO_ACTION_INTERVAL_MAX = 5.0f; // Tiempo máximo entre alternancia SHAPE/PHYSICS (escalado con resolución)
|
||||||
|
constexpr int LOGO_WEIGHT_TOGGLE_PHYSICS = 100; // Único peso: alternar SHAPE ↔ PHYSICS (100%)
|
||||||
|
|
||||||
|
// Sistema de convergencia para LOGO MODE (evita interrupciones prematuras en resoluciones altas)
|
||||||
|
constexpr float LOGO_CONVERGENCE_MIN = 0.75f; // 75% mínimo (permite algo de movimiento al disparar)
|
||||||
|
constexpr float LOGO_CONVERGENCE_MAX = 1.00f; // 100% máximo (completamente formado)
|
||||||
|
constexpr float LOGO_CONVERGENCE_DISTANCE = 20.0f; // Distancia (px) para considerar pelota "convergida" (más permisivo que SHAPE_NEAR)
|
||||||
|
|
||||||
|
// Probabilidad de salto a Logo Mode desde DEMO/DEMO_LITE (%)
|
||||||
|
// Relación DEMO:LOGO = 6:1 (pasa 6x más tiempo en DEMO que en LOGO)
|
||||||
|
constexpr int LOGO_JUMP_PROBABILITY_FROM_DEMO = 5; // 5% probabilidad en DEMO normal (más raro)
|
||||||
|
constexpr int LOGO_JUMP_PROBABILITY_FROM_DEMO_LITE = 3; // 3% probabilidad en DEMO LITE (aún más raro)
|
||||||
|
|
||||||
|
// Sistema de espera de flips en LOGO MODE (camino alternativo de decisión)
|
||||||
|
constexpr int LOGO_FLIP_WAIT_MIN = 1; // Mínimo de flips a esperar antes de cambiar a PHYSICS
|
||||||
|
constexpr int LOGO_FLIP_WAIT_MAX = 3; // Máximo de flips a esperar
|
||||||
|
constexpr float LOGO_FLIP_TRIGGER_MIN = 0.20f; // 20% mínimo de progreso de flip para trigger
|
||||||
|
constexpr float LOGO_FLIP_TRIGGER_MAX = 0.80f; // 80% máximo de progreso de flip para trigger
|
||||||
|
constexpr int LOGO_FLIP_WAIT_PROBABILITY = 50; // 50% probabilidad de elegir el camino "esperar flip"
|
||||||
|
|
||||||
|
// Configuración de AppLogo (logo periódico en pantalla)
|
||||||
|
constexpr float APPLOGO_DISPLAY_INTERVAL = 90.0f; // Intervalo entre apariciones del logo (segundos)
|
||||||
|
constexpr float APPLOGO_DISPLAY_DURATION = 30.0f; // Duración de visibilidad del logo (segundos)
|
||||||
|
constexpr float APPLOGO_ANIMATION_DURATION = 0.5f; // Duración de animación entrada/salida (segundos)
|
||||||
|
constexpr float APPLOGO_HEIGHT_PERCENT = 0.4f; // Altura del logo = 40% de la altura de pantalla
|
||||||
|
constexpr float APPLOGO_PADDING_PERCENT = 0.05f; // Padding desde esquina inferior-derecha = 10%
|
||||||
|
constexpr float APPLOGO_LOGO2_DELAY = 0.25f; // Retraso de Logo 2 respecto a Logo 1 (segundos)
|
||||||
|
|
||||||
|
// Configuración de Modo BOIDS (comportamiento de enjambre)
|
||||||
|
// TIME-BASED CONVERSION (frame-based → time-based):
|
||||||
|
// - Radios: sin cambios (píxeles)
|
||||||
|
// - Velocidades (MAX_SPEED, MIN_SPEED): ×60 (px/frame → px/s)
|
||||||
|
// - Aceleraciones puras (SEPARATION, COHESION): ×60² = ×3600 (px/frame² → px/s²)
|
||||||
|
// - Steering proporcional (ALIGNMENT): ×60 (proporcional a velocidad)
|
||||||
|
// - Límite velocidad (MAX_FORCE): ×60 (px/frame → px/s)
|
||||||
|
constexpr float BOID_SEPARATION_RADIUS = 30.0f; // Radio para evitar colisiones (píxeles)
|
||||||
|
constexpr float BOID_ALIGNMENT_RADIUS = 50.0f; // Radio para alinear velocidad con vecinos
|
||||||
|
constexpr float BOID_COHESION_RADIUS = 80.0f; // Radio para moverse hacia centro del grupo
|
||||||
|
constexpr float BOID_SEPARATION_WEIGHT = 5400.0f; // Aceleración de separación (px/s²) [era 1.5 × 3600]
|
||||||
|
constexpr float BOID_ALIGNMENT_WEIGHT = 60.0f; // Steering de alineación (proporcional) [era 1.0 × 60]
|
||||||
|
constexpr float BOID_COHESION_WEIGHT = 3.6f; // Aceleración de cohesión (px/s²) [era 0.001 × 3600]
|
||||||
|
constexpr float BOID_MAX_SPEED = 150.0f; // Velocidad máxima (px/s) [era 2.5 × 60]
|
||||||
|
constexpr float BOID_MAX_FORCE = 3.0f; // Fuerza máxima de steering (px/s) [era 0.05 × 60]
|
||||||
|
constexpr float BOID_MIN_SPEED = 18.0f; // Velocidad mínima (px/s) [era 0.3 × 60]
|
||||||
|
constexpr float BOID_BOUNDARY_MARGIN = 50.0f; // Distancia a borde para activar repulsión (píxeles)
|
||||||
|
constexpr float BOID_BOUNDARY_WEIGHT = 7200.0f; // Aceleración de repulsión de bordes (px/s²) [más fuerte que separation]
|
||||||
|
|
||||||
|
// FASE 2: Spatial Hash Grid para optimización O(n²) → O(n)
|
||||||
|
constexpr float BOID_GRID_CELL_SIZE = 100.0f; // Tamaño de celda del grid (píxeles)
|
||||||
|
// Debe ser ≥ BOID_COHESION_RADIUS para funcionar correctamente
|
||||||
|
|
||||||
|
constexpr float PI = 3.14159265358979323846f; // Constante PI
|
||||||
|
|
||||||
|
// Función auxiliar para obtener la ruta del directorio del ejecutable
|
||||||
|
#include <filesystem>
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
#include <limits.h>
|
||||||
|
#include <mach-o/dyld.h>
|
||||||
|
#else
|
||||||
|
#include <limits.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
inline std::string getExecutableDirectory() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
char buffer[MAX_PATH];
|
||||||
|
GetModuleFileNameA(NULL, buffer, MAX_PATH);
|
||||||
|
std::filesystem::path exe_path(buffer);
|
||||||
|
return exe_path.parent_path().string();
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
char buffer[PATH_MAX];
|
||||||
|
uint32_t size = sizeof(buffer);
|
||||||
|
if (_NSGetExecutablePath(buffer, &size) == 0) {
|
||||||
|
std::filesystem::path exe_path(buffer);
|
||||||
|
return exe_path.parent_path().string();
|
||||||
|
}
|
||||||
|
return ".";
|
||||||
|
#else
|
||||||
|
// Linux y otros Unix
|
||||||
|
char buffer[PATH_MAX];
|
||||||
|
ssize_t len = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1);
|
||||||
|
if (len != -1) {
|
||||||
|
buffer[len] = '\0';
|
||||||
|
std::filesystem::path exe_path(buffer);
|
||||||
|
return exe_path.parent_path().string();
|
||||||
|
}
|
||||||
|
return ".";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función auxiliar para obtener la ruta del directorio de recursos
|
||||||
|
inline std::string getResourcesDirectory() {
|
||||||
|
std::string exe_dir = getExecutableDirectory();
|
||||||
|
|
||||||
|
#ifdef MACOS_BUNDLE
|
||||||
|
// En macOS Bundle: ejecutable está en Contents/MacOS/, recursos en Contents/Resources/
|
||||||
|
std::filesystem::path resources_path = std::filesystem::path(exe_dir) / ".." / "Resources";
|
||||||
|
return resources_path.string();
|
||||||
|
#else
|
||||||
|
// En desarrollo o releases normales: recursos están junto al ejecutable
|
||||||
|
return exe_dir;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
2237
source/engine.cpp
131
source/engine.h
@@ -1,131 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <SDL3/SDL_events.h> // for SDL_Event
|
|
||||||
#include <SDL3/SDL_render.h> // for SDL_Renderer
|
|
||||||
#include <SDL3/SDL_stdinc.h> // for Uint64
|
|
||||||
#include <SDL3/SDL_video.h> // for SDL_Window
|
|
||||||
|
|
||||||
#include <array> // for array
|
|
||||||
#include <memory> // for unique_ptr, shared_ptr
|
|
||||||
#include <string> // for string
|
|
||||||
#include <vector> // for vector
|
|
||||||
|
|
||||||
#include "defines.h" // for GravityDirection, ColorTheme
|
|
||||||
#include "ball.h" // for Ball
|
|
||||||
#include "external/texture.h" // for Texture
|
|
||||||
|
|
||||||
class Engine {
|
|
||||||
public:
|
|
||||||
// Interfaz pública
|
|
||||||
bool initialize();
|
|
||||||
void run();
|
|
||||||
void shutdown();
|
|
||||||
|
|
||||||
private:
|
|
||||||
// Recursos SDL
|
|
||||||
SDL_Window* window_ = nullptr;
|
|
||||||
SDL_Renderer* renderer_ = nullptr;
|
|
||||||
std::shared_ptr<Texture> texture_ = nullptr;
|
|
||||||
|
|
||||||
// Estado del simulador
|
|
||||||
std::vector<std::unique_ptr<Ball>> balls_;
|
|
||||||
std::array<int, 8> test_ = {1, 10, 100, 500, 1000, 10000, 50000, 100000};
|
|
||||||
GravityDirection current_gravity_ = GravityDirection::DOWN;
|
|
||||||
int scenario_ = 0;
|
|
||||||
bool should_exit_ = false;
|
|
||||||
|
|
||||||
// Sistema de timing
|
|
||||||
Uint64 last_frame_time_ = 0;
|
|
||||||
float delta_time_ = 0.0f;
|
|
||||||
|
|
||||||
// UI y debug
|
|
||||||
bool show_debug_ = false;
|
|
||||||
bool show_text_ = true;
|
|
||||||
|
|
||||||
// Sistema de zoom dinámico
|
|
||||||
int current_window_zoom_ = WINDOW_ZOOM;
|
|
||||||
std::string text_;
|
|
||||||
int text_pos_ = 0;
|
|
||||||
Uint64 text_init_time_ = 0;
|
|
||||||
|
|
||||||
// FPS y V-Sync
|
|
||||||
Uint64 fps_last_time_ = 0;
|
|
||||||
int fps_frame_count_ = 0;
|
|
||||||
int fps_current_ = 0;
|
|
||||||
std::string fps_text_ = "FPS: 0";
|
|
||||||
bool vsync_enabled_ = true;
|
|
||||||
std::string vsync_text_ = "VSYNC ON";
|
|
||||||
bool fullscreen_enabled_ = false;
|
|
||||||
bool real_fullscreen_enabled_ = false;
|
|
||||||
|
|
||||||
// Auto-restart system
|
|
||||||
Uint64 all_balls_stopped_start_time_ = 0; // Momento cuando todas se pararon
|
|
||||||
bool all_balls_were_stopped_ = false; // Flag de estado anterior
|
|
||||||
static constexpr Uint64 AUTO_RESTART_DELAY = 5000; // 5 segundos en ms
|
|
||||||
|
|
||||||
// Resolución dinámica para modo real fullscreen
|
|
||||||
int current_screen_width_ = SCREEN_WIDTH;
|
|
||||||
int current_screen_height_ = SCREEN_HEIGHT;
|
|
||||||
|
|
||||||
// Sistema de temas
|
|
||||||
ColorTheme current_theme_ = ColorTheme::SUNSET;
|
|
||||||
|
|
||||||
// Estructura de tema de colores
|
|
||||||
struct ThemeColors {
|
|
||||||
float bg_top_r, bg_top_g, bg_top_b;
|
|
||||||
float bg_bottom_r, bg_bottom_g, bg_bottom_b;
|
|
||||||
std::vector<Color> ball_colors;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Temas de colores definidos
|
|
||||||
ThemeColors themes_[5];
|
|
||||||
|
|
||||||
// Sistema RotoBall (esfera 3D rotante)
|
|
||||||
SimulationMode current_mode_ = SimulationMode::PHYSICS;
|
|
||||||
struct RotoBallData {
|
|
||||||
float angle_y = 0.0f; // Ángulo de rotación en eje Y
|
|
||||||
float angle_x = 0.0f; // Ángulo de rotación en eje X
|
|
||||||
float transition_progress = 0.0f; // Progreso de transición (0.0-1.0)
|
|
||||||
bool transitioning = false; // ¿Está en transición?
|
|
||||||
};
|
|
||||||
RotoBallData rotoball_;
|
|
||||||
|
|
||||||
// Batch rendering
|
|
||||||
std::vector<SDL_Vertex> batch_vertices_;
|
|
||||||
std::vector<int> batch_indices_;
|
|
||||||
|
|
||||||
// Métodos principales del loop
|
|
||||||
void calculateDeltaTime();
|
|
||||||
void update();
|
|
||||||
void handleEvents();
|
|
||||||
void render();
|
|
||||||
|
|
||||||
// Métodos auxiliares
|
|
||||||
void initBalls(int value);
|
|
||||||
void setText();
|
|
||||||
void pushBallsAwayFromGravity();
|
|
||||||
void switchBallsGravity();
|
|
||||||
void changeGravityDirection(GravityDirection direction);
|
|
||||||
void toggleVSync();
|
|
||||||
void toggleFullscreen();
|
|
||||||
void toggleRealFullscreen();
|
|
||||||
std::string gravityDirectionToString(GravityDirection direction) const;
|
|
||||||
void initializeThemes();
|
|
||||||
void checkAutoRestart();
|
|
||||||
void performRandomRestart();
|
|
||||||
|
|
||||||
// Sistema de zoom dinámico
|
|
||||||
int calculateMaxWindowZoom() const;
|
|
||||||
void setWindowZoom(int new_zoom);
|
|
||||||
void zoomIn();
|
|
||||||
void zoomOut();
|
|
||||||
|
|
||||||
// Rendering
|
|
||||||
void renderGradientBackground();
|
|
||||||
void addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b);
|
|
||||||
|
|
||||||
// Sistema RotoBall
|
|
||||||
void toggleRotoBallMode();
|
|
||||||
void generateRotoBallSphere();
|
|
||||||
void updateRotoBall();
|
|
||||||
};
|
|
||||||
280
source/engine.hpp
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_events.h> // for SDL_Event
|
||||||
|
#include <SDL3/SDL_render.h> // for SDL_Renderer (ui_renderer_ software renderer)
|
||||||
|
#include <SDL3/SDL_stdinc.h> // for Uint64
|
||||||
|
#include <SDL3/SDL_surface.h> // for SDL_Surface (ui_surface_)
|
||||||
|
#include <SDL3/SDL_video.h> // for SDL_Window
|
||||||
|
|
||||||
|
#include <array> // for array
|
||||||
|
#include <memory> // for unique_ptr, shared_ptr
|
||||||
|
#include <string> // for string
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include "ball.hpp" // for Ball
|
||||||
|
#include "boids_mgr/boid_manager.hpp" // for BoidManager
|
||||||
|
#include "defines.hpp" // for GravityDirection, ColorTheme, ShapeType
|
||||||
|
#include "external/texture.hpp" // for Texture
|
||||||
|
#include "gpu/gpu_ball_buffer.hpp" // for GpuBallBuffer, BallGPUData
|
||||||
|
#include "gpu/gpu_context.hpp" // for GpuContext
|
||||||
|
#include "gpu/gpu_pipeline.hpp" // for GpuPipeline
|
||||||
|
#include "gpu/gpu_sprite_batch.hpp" // for GpuSpriteBatch
|
||||||
|
#include "gpu/gpu_texture.hpp" // for GpuTexture
|
||||||
|
#include "input/input_handler.hpp" // for InputHandler
|
||||||
|
#include "scene/scene_manager.hpp" // for SceneManager
|
||||||
|
#include "shapes_mgr/shape_manager.hpp" // for ShapeManager
|
||||||
|
#include "state/state_manager.hpp" // for StateManager
|
||||||
|
#include "theme_manager.hpp" // for ThemeManager
|
||||||
|
#include "ui/app_logo.hpp" // for AppLogo
|
||||||
|
#include "ui/ui_manager.hpp" // for UIManager
|
||||||
|
|
||||||
|
class Engine {
|
||||||
|
public:
|
||||||
|
// Interfaz pública principal
|
||||||
|
bool initialize(int width = 0, int height = 0, int zoom = 0, bool fullscreen = false, AppMode initial_mode = AppMode::SANDBOX);
|
||||||
|
void run();
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
// === Métodos públicos para InputHandler ===
|
||||||
|
|
||||||
|
// Gravedad y física
|
||||||
|
void pushBallsAwayFromGravity();
|
||||||
|
void handleGravityToggle();
|
||||||
|
void handleGravityDirectionChange(GravityDirection direction, const char* notification_text);
|
||||||
|
|
||||||
|
// Display y depuración
|
||||||
|
void toggleVSync();
|
||||||
|
void toggleDebug();
|
||||||
|
void toggleHelp();
|
||||||
|
|
||||||
|
// Figuras 3D
|
||||||
|
void toggleShapeMode();
|
||||||
|
void activateShape(ShapeType type, const char* notification_text);
|
||||||
|
void handleShapeScaleChange(bool increase);
|
||||||
|
void resetShapeScale();
|
||||||
|
void toggleDepthZoom();
|
||||||
|
|
||||||
|
// Boids (comportamiento de enjambre)
|
||||||
|
void toggleBoidsMode(bool force_gravity_on = true);
|
||||||
|
|
||||||
|
// Temas de colores
|
||||||
|
void cycleTheme(bool forward);
|
||||||
|
void switchThemeByNumpad(int numpad_key);
|
||||||
|
void toggleThemePage();
|
||||||
|
void pauseDynamicTheme();
|
||||||
|
|
||||||
|
// Sprites/Texturas
|
||||||
|
void switchTexture();
|
||||||
|
|
||||||
|
// Escenarios (número de pelotas)
|
||||||
|
void changeScenario(int scenario_id, const char* notification_text);
|
||||||
|
|
||||||
|
// Zoom y fullscreen
|
||||||
|
void handleZoomIn();
|
||||||
|
void handleZoomOut();
|
||||||
|
void toggleFullscreen();
|
||||||
|
void toggleRealFullscreen();
|
||||||
|
void toggleIntegerScaling();
|
||||||
|
|
||||||
|
// Campo de juego (tamaño lógico + físico)
|
||||||
|
void fieldSizeUp();
|
||||||
|
void fieldSizeDown();
|
||||||
|
void setFieldScale(float new_scale);
|
||||||
|
|
||||||
|
// PostFX presets
|
||||||
|
void handlePostFXCycle();
|
||||||
|
void handlePostFXToggle();
|
||||||
|
void setInitialPostFX(int mode);
|
||||||
|
void setPostFXParamOverrides(float vignette, float chroma);
|
||||||
|
|
||||||
|
// Cicle PostFX nadiu (OFF → Vinyeta → Scanlines → Cromàtica → Complet)
|
||||||
|
void cycleShader();
|
||||||
|
|
||||||
|
// Modo kiosko
|
||||||
|
void setKioskMode(bool enabled) { kiosk_mode_ = enabled; }
|
||||||
|
bool isKioskMode() const { return kiosk_mode_; }
|
||||||
|
|
||||||
|
// Escenario custom (tecla 9, --custom-balls)
|
||||||
|
void setCustomScenario(int balls);
|
||||||
|
bool isCustomScenarioEnabled() const { return custom_scenario_enabled_; }
|
||||||
|
bool isCustomAutoAvailable() const { return custom_auto_available_; }
|
||||||
|
int getCustomScenarioBalls() const { return custom_scenario_balls_; }
|
||||||
|
|
||||||
|
// Control manual del benchmark (--skip-benchmark, --max-balls)
|
||||||
|
void setSkipBenchmark();
|
||||||
|
void setMaxBallsOverride(int n);
|
||||||
|
|
||||||
|
// Notificaciones (público para InputHandler)
|
||||||
|
void showNotificationForAction(const std::string& text);
|
||||||
|
|
||||||
|
// Modos de aplicación (DEMO/LOGO)
|
||||||
|
void toggleDemoMode();
|
||||||
|
void toggleDemoLiteMode();
|
||||||
|
void toggleLogoMode();
|
||||||
|
|
||||||
|
// === Métodos públicos para StateManager (automatización DEMO/LOGO sin notificación) ===
|
||||||
|
void enterShapeMode(ShapeType type); // Activar figura (sin notificación)
|
||||||
|
void exitShapeMode(bool force_gravity = true); // Volver a física (sin notificación)
|
||||||
|
void switchTextureSilent(); // Cambiar textura (sin notificación)
|
||||||
|
void setTextureByIndex(size_t index); // Restaurar textura específica
|
||||||
|
|
||||||
|
// === Getters públicos para UIManager (Debug HUD) ===
|
||||||
|
bool getVSyncEnabled() const { return vsync_enabled_; }
|
||||||
|
bool getFullscreenEnabled() const { return fullscreen_enabled_; }
|
||||||
|
bool getRealFullscreenEnabled() const { return real_fullscreen_enabled_; }
|
||||||
|
ScalingMode getCurrentScalingMode() const { return current_scaling_mode_; }
|
||||||
|
int getCurrentScreenWidth() const { return current_screen_width_; }
|
||||||
|
int getCurrentScreenHeight() const { return current_screen_height_; }
|
||||||
|
std::string getCurrentTextureName() const {
|
||||||
|
if (texture_names_.empty()) return "";
|
||||||
|
return texture_names_[current_texture_index_];
|
||||||
|
}
|
||||||
|
int getBaseScreenWidth() const { return base_screen_width_; }
|
||||||
|
int getBaseScreenHeight() const { return base_screen_height_; }
|
||||||
|
int getMaxAutoScenario() const { return max_auto_scenario_; }
|
||||||
|
size_t getCurrentTextureIndex() const { return current_texture_index_; }
|
||||||
|
bool isPostFXEnabled() const { return postfx_enabled_; }
|
||||||
|
int getPostFXMode() const { return postfx_effect_mode_; }
|
||||||
|
float getPostFXVignette() const { return postfx_uniforms_.vignette_strength; }
|
||||||
|
float getPostFXChroma() const { return postfx_uniforms_.chroma_strength; }
|
||||||
|
float getPostFXScanline() const { return postfx_uniforms_.scanline_strength; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// === Componentes del sistema (Composición) ===
|
||||||
|
std::unique_ptr<InputHandler> input_handler_; // Manejo de entradas SDL
|
||||||
|
std::unique_ptr<SceneManager> scene_manager_; // Gestión de bolas y física
|
||||||
|
std::unique_ptr<ShapeManager> shape_manager_; // Gestión de figuras 3D
|
||||||
|
std::unique_ptr<BoidManager> boid_manager_; // Gestión de comportamiento boids
|
||||||
|
std::unique_ptr<StateManager> state_manager_; // Gestión de estados (DEMO/LOGO)
|
||||||
|
std::unique_ptr<UIManager> ui_manager_; // Gestión de UI (HUD, FPS, notificaciones)
|
||||||
|
std::unique_ptr<AppLogo> app_logo_; // Gestión de logo periódico en pantalla
|
||||||
|
|
||||||
|
// === SDL window ===
|
||||||
|
SDL_Window* window_ = nullptr;
|
||||||
|
|
||||||
|
// === SDL_GPU rendering pipeline ===
|
||||||
|
std::unique_ptr<GpuContext> gpu_ctx_; // Device + swapchain
|
||||||
|
std::unique_ptr<GpuPipeline> gpu_pipeline_; // Sprite + ball + postfx pipelines
|
||||||
|
std::unique_ptr<GpuSpriteBatch> sprite_batch_; // Per-frame vertex/index batch (bg + shape + UI)
|
||||||
|
std::unique_ptr<GpuBallBuffer> gpu_ball_buffer_; // Instanced ball instance data (PHYSICS/BOIDS)
|
||||||
|
std::vector<BallGPUData> ball_gpu_data_; // CPU-side staging vector (reused each frame)
|
||||||
|
std::unique_ptr<GpuTexture> offscreen_tex_; // Offscreen render target (Pass 1)
|
||||||
|
std::unique_ptr<GpuTexture> white_tex_; // 1×1 white (background gradient)
|
||||||
|
std::unique_ptr<GpuTexture> ui_tex_; // UI text overlay texture
|
||||||
|
|
||||||
|
// GPU sprite textures (one per ball skin, parallel to textures_/texture_names_)
|
||||||
|
std::unique_ptr<GpuTexture> gpu_texture_; // Active GPU sprite texture
|
||||||
|
std::vector<std::unique_ptr<GpuTexture>> gpu_textures_; // All GPU sprite textures
|
||||||
|
|
||||||
|
// === SDL_Renderer (software, for UI text via SDL3_ttf) ===
|
||||||
|
// Renders to ui_surface_, then uploaded as gpu texture overlay.
|
||||||
|
SDL_Renderer* ui_renderer_ = nullptr;
|
||||||
|
SDL_Surface* ui_surface_ = nullptr;
|
||||||
|
|
||||||
|
// Legacy Texture objects — kept for ball physics sizing and AppLogo
|
||||||
|
std::shared_ptr<Texture> texture_ = nullptr; // Textura activa actual
|
||||||
|
std::vector<std::shared_ptr<Texture>> textures_; // Todas las texturas disponibles
|
||||||
|
std::vector<std::string> texture_names_; // Nombres de texturas (sin extensión)
|
||||||
|
size_t current_texture_index_ = 0; // Índice de textura activa
|
||||||
|
int current_ball_size_ = 10; // Tamaño actual de pelotas (dinámico)
|
||||||
|
|
||||||
|
// Estado del simulador
|
||||||
|
bool should_exit_ = false;
|
||||||
|
|
||||||
|
// Sistema de timing
|
||||||
|
Uint64 last_frame_time_ = 0;
|
||||||
|
float delta_time_ = 0.0f;
|
||||||
|
|
||||||
|
// PostFX uniforms (passed to GPU each frame)
|
||||||
|
PostFXUniforms postfx_uniforms_ = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
|
int postfx_effect_mode_ = 3;
|
||||||
|
bool postfx_enabled_ = false;
|
||||||
|
float postfx_override_vignette_ = -1.f; // -1 = sin override
|
||||||
|
float postfx_override_chroma_ = -1.f;
|
||||||
|
|
||||||
|
// Sistema de escala de ventana
|
||||||
|
float current_window_scale_ = 1.0f;
|
||||||
|
|
||||||
|
// Escala del campo de juego lógico (F7/F8)
|
||||||
|
float current_field_scale_ = 1.0f;
|
||||||
|
|
||||||
|
// V-Sync y fullscreen
|
||||||
|
bool vsync_enabled_ = true;
|
||||||
|
bool fullscreen_enabled_ = false;
|
||||||
|
bool real_fullscreen_enabled_ = false;
|
||||||
|
bool kiosk_mode_ = false;
|
||||||
|
ScalingMode current_scaling_mode_ = ScalingMode::INTEGER;
|
||||||
|
|
||||||
|
// Resolución base (configurada por CLI o default)
|
||||||
|
int base_screen_width_ = DEFAULT_SCREEN_WIDTH;
|
||||||
|
int base_screen_height_ = DEFAULT_SCREEN_HEIGHT;
|
||||||
|
|
||||||
|
// Resolución dinámica actual (cambia en fullscreen real)
|
||||||
|
int current_screen_width_ = DEFAULT_SCREEN_WIDTH;
|
||||||
|
int current_screen_height_ = DEFAULT_SCREEN_HEIGHT;
|
||||||
|
|
||||||
|
// Resolución física real de ventana/pantalla (para texto absoluto)
|
||||||
|
int physical_window_width_ = DEFAULT_SCREEN_WIDTH;
|
||||||
|
int physical_window_height_ = DEFAULT_SCREEN_HEIGHT;
|
||||||
|
|
||||||
|
// Sistema de temas (delegado a ThemeManager)
|
||||||
|
std::unique_ptr<ThemeManager> theme_manager_;
|
||||||
|
int theme_page_ = 0;
|
||||||
|
|
||||||
|
// Modo de simulación actual (PHYSICS/SHAPE/BOIDS)
|
||||||
|
SimulationMode current_mode_ = SimulationMode::PHYSICS;
|
||||||
|
|
||||||
|
// Sistema de Modo DEMO (auto-play) y LOGO
|
||||||
|
int max_auto_scenario_ = 5;
|
||||||
|
|
||||||
|
// Escenario custom (--custom-balls)
|
||||||
|
int custom_scenario_balls_ = 0;
|
||||||
|
bool custom_scenario_enabled_ = false;
|
||||||
|
bool custom_auto_available_ = false;
|
||||||
|
bool skip_benchmark_ = false;
|
||||||
|
|
||||||
|
// Bucket sort per z-ordering (SHAPE mode)
|
||||||
|
static constexpr int DEPTH_SORT_BUCKETS = 256;
|
||||||
|
std::array<std::vector<size_t>, DEPTH_SORT_BUCKETS> depth_buckets_;
|
||||||
|
|
||||||
|
// Métodos principales del loop
|
||||||
|
void calculateDeltaTime();
|
||||||
|
void update();
|
||||||
|
void render();
|
||||||
|
|
||||||
|
// Benchmark de rendimiento (determina max_auto_scenario_ al inicio)
|
||||||
|
void runPerformanceBenchmark();
|
||||||
|
|
||||||
|
// Métodos auxiliares privados
|
||||||
|
|
||||||
|
// Sistema de cambio de sprites dinámico
|
||||||
|
void switchTextureInternal(bool show_notification);
|
||||||
|
|
||||||
|
// Sistema de escala de ventana
|
||||||
|
float calculateMaxWindowScale() const;
|
||||||
|
void setWindowScale(float new_scale);
|
||||||
|
void zoomIn();
|
||||||
|
void zoomOut();
|
||||||
|
void updatePhysicalWindowSize();
|
||||||
|
|
||||||
|
// Rendering (GPU path replaces addSpriteToBatch)
|
||||||
|
void addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale = 1.0f);
|
||||||
|
|
||||||
|
// Sistema de Figuras 3D
|
||||||
|
void toggleShapeModeInternal(bool force_gravity_on_exit = true);
|
||||||
|
void activateShapeInternal(ShapeType type);
|
||||||
|
void updateShape();
|
||||||
|
void generateShape();
|
||||||
|
|
||||||
|
// PostFX helper
|
||||||
|
void applyPostFXPreset(int mode);
|
||||||
|
|
||||||
|
// Boids: comprueba si un escenario tiene ≤ BOIDS_MAX_BALLS bolas
|
||||||
|
bool isScenarioAllowedForBoids(int scenario_id) const;
|
||||||
|
|
||||||
|
// GPU helpers
|
||||||
|
bool loadGpuSpriteTexture(size_t index); // Upload one sprite texture to GPU
|
||||||
|
void recreateOffscreenTexture(); // Recreate when resolution changes
|
||||||
|
void renderUIToSurface(); // Render text/UI to ui_surface_
|
||||||
|
void uploadUISurface(SDL_GPUCommandBuffer* cmd_buf); // Upload ui_surface_ → ui_tex_
|
||||||
|
};
|
||||||
78
source/external/dbgtxt.h
vendored
@@ -1,78 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
SDL_Texture* dbg_tex = nullptr;
|
|
||||||
SDL_Renderer* dbg_ren = nullptr;
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
inline void dbg_init(SDL_Renderer* renderer) {
|
|
||||||
dbg_ren = renderer;
|
|
||||||
Uint8 font[448] = {0x42, 0x4D, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x82, 0x01, 0x00, 0x00, 0x12, 0x0B, 0x00, 0x00, 0x12, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x18, 0xF3, 0x83, 0x83, 0xCF, 0x83, 0x87, 0x00, 0x00, 0xF3, 0x39, 0x39, 0xCF, 0x79, 0xF3, 0x00, 0x00, 0x01, 0xF9, 0x39, 0xCF, 0x61, 0xF9, 0x00, 0x00, 0x33, 0xF9, 0x03, 0xE7, 0x87, 0x81, 0x00, 0x00, 0x93, 0x03, 0x3F, 0xF3, 0x1B, 0x39, 0x00, 0x00, 0xC3, 0x3F, 0x9F, 0x39, 0x3B, 0x39, 0x00, 0x41, 0xE3, 0x03, 0xC3, 0x01, 0x87, 0x83, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xE7, 0x01, 0xC7, 0x81, 0x01, 0x83, 0x00, 0x00, 0xE7, 0x1F, 0x9B, 0xE7, 0x1F, 0x39, 0x00, 0x00, 0xE7, 0x8F, 0x39, 0xE7, 0x87, 0xF9, 0x00, 0x00, 0xC3, 0xC7, 0x39, 0xE7, 0xC3, 0xC3, 0x00, 0x00, 0x99, 0xE3, 0x39, 0xE7, 0xF1, 0xE7, 0x00, 0x00, 0x99, 0xF1, 0xB3, 0xC7, 0x39, 0xF3, 0x00, 0x00, 0x99, 0x01, 0xC7, 0xE7, 0x83, 0x81, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xE7, 0x83, 0xEF, 0x39, 0x39, 0x00, 0x00, 0x39, 0xE7, 0x39, 0xC7, 0x11, 0x11, 0x00, 0x00, 0xF9, 0xE7, 0x39, 0x83, 0x01, 0x83, 0x00, 0x00, 0x83, 0xE7, 0x39, 0x11, 0x01, 0xC7, 0x00, 0x00, 0x3F, 0xE7, 0x39, 0x39, 0x29, 0x83, 0x00, 0x00, 0x33, 0xE7, 0x39, 0x39, 0x39, 0x11, 0x00, 0x00, 0x87, 0x81, 0x39, 0x39, 0x39, 0x39, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x39, 0x39, 0x83, 0x3F, 0x85, 0x31, 0x00, 0x00, 0x39, 0x31, 0x39, 0x3F, 0x33, 0x23, 0x00, 0x00, 0x29, 0x21, 0x39, 0x03, 0x21, 0x07, 0x00, 0x00, 0x01, 0x01, 0x39, 0x39, 0x39, 0x31, 0x00, 0x00, 0x01, 0x09, 0x39, 0x39, 0x39, 0x39, 0x00, 0x00, 0x11, 0x19, 0x39, 0x39, 0x39, 0x39, 0x00, 0x00, 0x39, 0x39, 0x83, 0x03, 0x83, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xC1, 0x39, 0x81, 0x83, 0x31, 0x01, 0x00, 0x00, 0x99, 0x39, 0xE7, 0x39, 0x23, 0x3F, 0x00, 0x00, 0x39, 0x39, 0xE7, 0xF9, 0x07, 0x3F, 0x00, 0x00, 0x31, 0x01, 0xE7, 0xF9, 0x0F, 0x3F, 0x00, 0x00, 0x3F, 0x39, 0xE7, 0xF9, 0x27, 0x3F, 0x00, 0x00, 0x9F, 0x39, 0xE7, 0xF9, 0x33, 0x3F, 0x00, 0x00, 0xC1, 0x39, 0x81, 0xF9, 0x39, 0x3F, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x39, 0x03, 0xC3, 0x07, 0x01, 0x3F, 0x00, 0x00, 0x39, 0x39, 0x99, 0x33, 0x3F, 0x3F, 0x00, 0x00, 0x01, 0x39, 0x3F, 0x39, 0x3F, 0x3F, 0x00, 0x00, 0x39, 0x03, 0x3F, 0x39, 0x03, 0x03, 0x00, 0x00, 0x39, 0x39, 0x3F, 0x39, 0x3F, 0x3F, 0x00, 0x00, 0x93, 0x39, 0x99, 0x33, 0x3F, 0x3F, 0x00, 0x00, 0xC7, 0x03, 0xC3, 0x07, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00};
|
|
||||||
|
|
||||||
// Cargar surface del bitmap font
|
|
||||||
SDL_Surface* font_surface = SDL_LoadBMP_IO(SDL_IOFromMem(font, 448), 1);
|
|
||||||
if (font_surface != nullptr) {
|
|
||||||
// Crear una nueva surface de 32 bits con canal alpha
|
|
||||||
SDL_Surface* rgba_surface = SDL_CreateSurface(font_surface->w, font_surface->h, SDL_PIXELFORMAT_RGBA8888);
|
|
||||||
if (rgba_surface != nullptr) {
|
|
||||||
// Obtener píxeles de ambas surfaces
|
|
||||||
Uint8* src_pixels = (Uint8*)font_surface->pixels;
|
|
||||||
Uint32* dst_pixels = (Uint32*)rgba_surface->pixels;
|
|
||||||
|
|
||||||
int width = font_surface->w;
|
|
||||||
int height = font_surface->h;
|
|
||||||
|
|
||||||
// Procesar cada píxel
|
|
||||||
for (int y = 0; y < height; y++) {
|
|
||||||
for (int x = 0; x < width; x++) {
|
|
||||||
int byte_index = y * font_surface->pitch + (x / 8);
|
|
||||||
int bit_index = 7 - (x % 8);
|
|
||||||
|
|
||||||
// Extraer bit del bitmap monocromo
|
|
||||||
bool is_white = (src_pixels[byte_index] >> bit_index) & 1;
|
|
||||||
|
|
||||||
if (is_white) // Fondo blanco original -> transparente
|
|
||||||
{
|
|
||||||
dst_pixels[y * width + x] = 0x00000000; // Transparente
|
|
||||||
} else // Texto negro original -> blanco opaco
|
|
||||||
{
|
|
||||||
dst_pixels[y * width + x] = 0xFFFFFFFF; // Blanco opaco
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dbg_tex = SDL_CreateTextureFromSurface(dbg_ren, rgba_surface);
|
|
||||||
SDL_DestroySurface(rgba_surface);
|
|
||||||
}
|
|
||||||
SDL_DestroySurface(font_surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configurar filtro nearest neighbor para píxel perfect del texto
|
|
||||||
if (dbg_tex != nullptr) {
|
|
||||||
SDL_SetTextureScaleMode(dbg_tex, SDL_SCALEMODE_NEAREST);
|
|
||||||
// Configurar blend mode para transparencia normal
|
|
||||||
SDL_SetTextureBlendMode(dbg_tex, SDL_BLENDMODE_BLEND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void dbg_print(int x, int y, const char* text, Uint8 r, Uint8 g, Uint8 b) {
|
|
||||||
int cc = 0;
|
|
||||||
SDL_SetTextureColorMod(dbg_tex, r, g, b);
|
|
||||||
SDL_FRect src = {0, 0, 8, 8};
|
|
||||||
SDL_FRect dst = {static_cast<float>(x), static_cast<float>(y), 8, 8};
|
|
||||||
while (text[cc] != 0) {
|
|
||||||
if (text[cc] != 32) {
|
|
||||||
if (text[cc] >= 65) {
|
|
||||||
src.x = ((text[cc] - 65) % 6) * 8;
|
|
||||||
src.y = ((text[cc] - 65) / 6) * 8;
|
|
||||||
} else {
|
|
||||||
src.x = ((text[cc] - 22) % 6) * 8;
|
|
||||||
src.y = ((text[cc] - 22) / 6) * 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_RenderTexture(dbg_ren, dbg_tex, &src, &dst);
|
|
||||||
}
|
|
||||||
cc++;
|
|
||||||
dst.x += 8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
27
source/external/mouse.cpp
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#include "mouse.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h> // Para SDL_GetTicks, Uint32, SDL_HideCursor, SDL_ShowCursor
|
||||||
|
|
||||||
|
namespace Mouse {
|
||||||
|
Uint32 cursor_hide_time = 3000; // Tiempo en milisegundos para ocultar el cursor
|
||||||
|
Uint32 last_mouse_move_time = 0; // Última vez que el ratón se movió
|
||||||
|
bool cursor_visible = true; // Estado del cursor
|
||||||
|
|
||||||
|
void handleEvent(const SDL_Event &event) {
|
||||||
|
if (event.type == SDL_EVENT_MOUSE_MOTION) {
|
||||||
|
last_mouse_move_time = SDL_GetTicks();
|
||||||
|
if (!cursor_visible) {
|
||||||
|
SDL_ShowCursor();
|
||||||
|
cursor_visible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateCursorVisibility() {
|
||||||
|
Uint32 current_time = SDL_GetTicks();
|
||||||
|
if (cursor_visible && (current_time - last_mouse_move_time > cursor_hide_time)) {
|
||||||
|
SDL_HideCursor();
|
||||||
|
cursor_visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace Mouse
|
||||||
15
source/external/mouse.hpp
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h> // Para Uint32, SDL_Event
|
||||||
|
|
||||||
|
// --- Namespace Mouse: gestión del ratón ---
|
||||||
|
namespace Mouse {
|
||||||
|
// --- Variables de estado del cursor ---
|
||||||
|
extern Uint32 cursor_hide_time; // Tiempo en milisegundos para ocultar el cursor tras inactividad
|
||||||
|
extern Uint32 last_mouse_move_time; // Última vez (en ms) que el ratón se movió
|
||||||
|
extern bool cursor_visible; // Indica si el cursor está visible
|
||||||
|
|
||||||
|
// --- Funciones ---
|
||||||
|
void handleEvent(const SDL_Event &event); // Procesa eventos de ratón (movimiento, clic, etc.)
|
||||||
|
void updateCursorVisibility(); // Actualiza la visibilidad del cursor según la inactividad
|
||||||
|
} // namespace Mouse
|
||||||
4
source/external/sprite.cpp
vendored
@@ -1,6 +1,6 @@
|
|||||||
#include "sprite.h"
|
#include "sprite.hpp"
|
||||||
|
|
||||||
#include "texture.h" // for Texture
|
#include "texture.hpp" // for Texture
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
Sprite::Sprite(std::shared_ptr<Texture> texture)
|
Sprite::Sprite(std::shared_ptr<Texture> texture)
|
||||||
|
|||||||
@@ -32,4 +32,7 @@ class Sprite {
|
|||||||
|
|
||||||
// Modulación de color
|
// Modulación de color
|
||||||
void setColor(int r, int g, int b);
|
void setColor(int r, int g, int b);
|
||||||
|
|
||||||
|
// Cambio de textura dinámico
|
||||||
|
void setTexture(std::shared_ptr<Texture> texture) { texture_ = texture; }
|
||||||
};
|
};
|
||||||
10630
source/external/stb_image_resize2.h
vendored
Normal file
41
source/external/texture.cpp
vendored
@@ -1,5 +1,5 @@
|
|||||||
#define STB_IMAGE_IMPLEMENTATION
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
#include "texture.h"
|
#include "texture.hpp"
|
||||||
|
|
||||||
#include <SDL3/SDL_error.h> // Para SDL_GetError
|
#include <SDL3/SDL_error.h> // Para SDL_GetError
|
||||||
#include <SDL3/SDL_log.h> // Para SDL_Log
|
#include <SDL3/SDL_log.h> // Para SDL_Log
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <string> // Para operator<<, string
|
#include <string> // Para operator<<, string
|
||||||
|
|
||||||
#include "stb_image.h" // Para stbi_failure_reason, stbi_image_free
|
#include "stb_image.h" // Para stbi_failure_reason, stbi_image_free
|
||||||
|
#include "resource_manager.hpp" // Sistema de empaquetado de recursos centralizado
|
||||||
|
|
||||||
Texture::Texture(SDL_Renderer *renderer)
|
Texture::Texture(SDL_Renderer *renderer)
|
||||||
: renderer_(renderer),
|
: renderer_(renderer),
|
||||||
@@ -36,12 +37,27 @@ bool Texture::loadFromFile(const std::string &file_path) {
|
|||||||
const std::string filename = file_path.substr(file_path.find_last_of("\\/") + 1);
|
const std::string filename = file_path.substr(file_path.find_last_of("\\/") + 1);
|
||||||
int req_format = STBI_rgb_alpha;
|
int req_format = STBI_rgb_alpha;
|
||||||
int width, height, orig_format;
|
int width, height, orig_format;
|
||||||
unsigned char *data = stbi_load(file_path.c_str(), &width, &height, &orig_format, req_format);
|
unsigned char *data = nullptr;
|
||||||
|
|
||||||
|
// 1. Intentar cargar desde ResourceManager (pack o disco)
|
||||||
|
unsigned char* resourceData = nullptr;
|
||||||
|
size_t resourceSize = 0;
|
||||||
|
|
||||||
|
if (ResourceManager::loadResource(file_path, resourceData, resourceSize)) {
|
||||||
|
// Descodificar imagen desde memoria usando stb_image
|
||||||
|
data = stbi_load_from_memory(resourceData, static_cast<int>(resourceSize),
|
||||||
|
&width, &height, &orig_format, req_format);
|
||||||
|
delete[] resourceData; // Liberar buffer temporal
|
||||||
|
|
||||||
|
if (data != nullptr) {
|
||||||
|
std::cout << "[Textura] " << filename << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Si todo falla, error
|
||||||
if (data == nullptr) {
|
if (data == nullptr) {
|
||||||
SDL_Log("Error al cargar la imagen: %s", stbi_failure_reason());
|
SDL_Log("Error al cargar la imagen: %s", stbi_failure_reason());
|
||||||
exit(1);
|
exit(1);
|
||||||
} else {
|
|
||||||
std::cout << "Imagen cargada: " << filename.c_str() << std::endl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int pitch;
|
int pitch;
|
||||||
@@ -76,6 +92,9 @@ bool Texture::loadFromFile(const std::string &file_path) {
|
|||||||
|
|
||||||
// Configurar filtro nearest neighbor para píxel perfect
|
// Configurar filtro nearest neighbor para píxel perfect
|
||||||
SDL_SetTextureScaleMode(new_texture, SDL_SCALEMODE_NEAREST);
|
SDL_SetTextureScaleMode(new_texture, SDL_SCALEMODE_NEAREST);
|
||||||
|
|
||||||
|
// Habilitar alpha blending para transparencias
|
||||||
|
SDL_SetTextureBlendMode(new_texture, SDL_BLENDMODE_BLEND);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destruye la superficie cargada
|
// Destruye la superficie cargada
|
||||||
@@ -117,3 +136,17 @@ int Texture::getHeight() {
|
|||||||
void Texture::setColor(int r, int g, int b) {
|
void Texture::setColor(int r, int g, int b) {
|
||||||
SDL_SetTextureColorMod(texture_, r, g, b);
|
SDL_SetTextureColorMod(texture_, r, g, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modula el alpha de la textura
|
||||||
|
void Texture::setAlpha(int alpha) {
|
||||||
|
if (texture_ != nullptr) {
|
||||||
|
SDL_SetTextureAlphaMod(texture_, static_cast<Uint8>(alpha));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configurar modo de escalado
|
||||||
|
void Texture::setScaleMode(SDL_ScaleMode mode) {
|
||||||
|
if (texture_ != nullptr) {
|
||||||
|
SDL_SetTextureScaleMode(texture_, mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <SDL3/SDL_render.h> // Para SDL_Renderer, SDL_Texture
|
#include <SDL3/SDL_render.h> // Para SDL_Renderer, SDL_Texture
|
||||||
|
|
||||||
#include <string> // Para std::string
|
#include <string> // Para std::string
|
||||||
|
#include <vector> // Para std::vector
|
||||||
|
|
||||||
class Texture {
|
class Texture {
|
||||||
private:
|
private:
|
||||||
@@ -38,6 +39,12 @@ class Texture {
|
|||||||
// Modula el color de la textura
|
// Modula el color de la textura
|
||||||
void setColor(int r, int g, int b);
|
void setColor(int r, int g, int b);
|
||||||
|
|
||||||
|
// Modula el alpha (transparencia) de la textura
|
||||||
|
void setAlpha(int alpha);
|
||||||
|
|
||||||
|
// Configurar modo de escalado (NEAREST para pixel art, LINEAR para suavizado)
|
||||||
|
void setScaleMode(SDL_ScaleMode mode);
|
||||||
|
|
||||||
// Getter para batch rendering
|
// Getter para batch rendering
|
||||||
SDL_Texture *getSDLTexture() const { return texture_; }
|
SDL_Texture *getSDLTexture() const { return texture_; }
|
||||||
};
|
};
|
||||||
77
source/gpu/gpu_ball_buffer.cpp
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#include "gpu_ball_buffer.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_log.h>
|
||||||
|
|
||||||
|
#include <algorithm> // std::min
|
||||||
|
#include <cstring> // memcpy
|
||||||
|
|
||||||
|
auto GpuBallBuffer::init(SDL_GPUDevice* device) -> bool {
|
||||||
|
Uint32 buf_size = static_cast<Uint32>(MAX_BALLS) * sizeof(BallGPUData);
|
||||||
|
|
||||||
|
// GPU vertex buffer (instance-rate data read by the ball instanced shader)
|
||||||
|
SDL_GPUBufferCreateInfo buf_info = {};
|
||||||
|
buf_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
|
||||||
|
buf_info.size = buf_size;
|
||||||
|
gpu_buf_ = SDL_CreateGPUBuffer(device, &buf_info);
|
||||||
|
if (gpu_buf_ == nullptr) {
|
||||||
|
SDL_Log("GpuBallBuffer: GPU buffer creation failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfer buffer (upload staging, cycled every frame)
|
||||||
|
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||||
|
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||||
|
tb_info.size = buf_size;
|
||||||
|
transfer_buf_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
|
||||||
|
if (transfer_buf_ == nullptr) {
|
||||||
|
SDL_Log("GpuBallBuffer: transfer buffer creation failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Log("GpuBallBuffer: initialized (capacity %d balls, %.1f MB VRAM)",
|
||||||
|
MAX_BALLS,
|
||||||
|
buf_size / (1024.0f * 1024.0f));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuBallBuffer::destroy(SDL_GPUDevice* device) {
|
||||||
|
if (device == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (transfer_buf_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTransferBuffer(device, transfer_buf_);
|
||||||
|
transfer_buf_ = nullptr;
|
||||||
|
}
|
||||||
|
if (gpu_buf_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUBuffer(device, gpu_buf_);
|
||||||
|
gpu_buf_ = nullptr;
|
||||||
|
}
|
||||||
|
count_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuBallBuffer::upload(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd, const BallGPUData* data, int count) -> bool {
|
||||||
|
if ((data == nullptr) || count <= 0) {
|
||||||
|
count_ = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
count = std::min(count, MAX_BALLS);
|
||||||
|
|
||||||
|
Uint32 upload_size = static_cast<Uint32>(count) * sizeof(BallGPUData);
|
||||||
|
|
||||||
|
void* ptr = SDL_MapGPUTransferBuffer(device, transfer_buf_, true /* cycle */);
|
||||||
|
if (ptr == nullptr) {
|
||||||
|
SDL_Log("GpuBallBuffer: transfer buffer map failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(ptr, data, upload_size);
|
||||||
|
SDL_UnmapGPUTransferBuffer(device, transfer_buf_);
|
||||||
|
|
||||||
|
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd);
|
||||||
|
SDL_GPUTransferBufferLocation src = {transfer_buf_, 0};
|
||||||
|
SDL_GPUBufferRegion dst = {gpu_buf_, 0, upload_size};
|
||||||
|
SDL_UploadToGPUBuffer(copy, &src, &dst, true /* cycle */);
|
||||||
|
SDL_EndGPUCopyPass(copy);
|
||||||
|
|
||||||
|
count_ = count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
47
source/gpu/gpu_ball_buffer.hpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// BallGPUData — 32-byte per-instance record stored in VRAM.
|
||||||
|
// Positions and sizes pre-converted to NDC space on CPU so the vertex shader
|
||||||
|
// needs no screen-dimension uniform.
|
||||||
|
// cx, cy : NDC center (cx = (x + w/2)/sw*2-1, cy = 1-(y+h/2)/sh*2)
|
||||||
|
// hw, hh : NDC half-size (hw = w/sw, hh = h/sh, both positive)
|
||||||
|
// r,g,b,a: RGBA in [0,1]
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct BallGPUData {
|
||||||
|
float cx, cy; // NDC center
|
||||||
|
float hw, hh; // NDC half-size (positive)
|
||||||
|
float r, g, b, a; // RGBA color [0,1]
|
||||||
|
};
|
||||||
|
static_assert(sizeof(BallGPUData) == 32, "BallGPUData must be 32 bytes");
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GpuBallBuffer — owns the GPU vertex buffer used for instanced ball rendering.
|
||||||
|
//
|
||||||
|
// Usage per frame:
|
||||||
|
// buffer.upload(device, cmd, data, count); // inside a copy pass
|
||||||
|
// // Then in render pass: bind buffer, SDL_DrawGPUPrimitives(pass, 6, count, 0, 0)
|
||||||
|
// ============================================================================
|
||||||
|
class GpuBallBuffer {
|
||||||
|
public:
|
||||||
|
static constexpr int MAX_BALLS = 500000;
|
||||||
|
|
||||||
|
bool init(SDL_GPUDevice* device);
|
||||||
|
void destroy(SDL_GPUDevice* device);
|
||||||
|
|
||||||
|
// Upload ball array to GPU via an internal copy pass.
|
||||||
|
// count is clamped to MAX_BALLS. Returns false on error or empty input.
|
||||||
|
bool upload(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd, const BallGPUData* data, int count);
|
||||||
|
|
||||||
|
SDL_GPUBuffer* buffer() const { return gpu_buf_; }
|
||||||
|
int count() const { return count_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
SDL_GPUBuffer* gpu_buf_ = nullptr;
|
||||||
|
SDL_GPUTransferBuffer* transfer_buf_ = nullptr;
|
||||||
|
int count_ = 0;
|
||||||
|
};
|
||||||
78
source/gpu/gpu_context.cpp
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#include "gpu_context.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_log.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
auto GpuContext::init(SDL_Window* window) -> bool {
|
||||||
|
window_ = window;
|
||||||
|
|
||||||
|
// Create GPU device: Metal on Apple, Vulkan elsewhere
|
||||||
|
#ifdef __APPLE__
|
||||||
|
SDL_GPUShaderFormat preferred = SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_METALLIB;
|
||||||
|
#else
|
||||||
|
SDL_GPUShaderFormat preferred = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||||
|
#endif
|
||||||
|
device_ = SDL_CreateGPUDevice(preferred, false, nullptr);
|
||||||
|
if (device_ == nullptr) {
|
||||||
|
std::cerr << "GpuContext: SDL_CreateGPUDevice failed: " << SDL_GetError() << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::cout << "GpuContext: driver = " << SDL_GetGPUDeviceDriver(device_) << '\n';
|
||||||
|
|
||||||
|
// Claim the window so the GPU device owns its swapchain
|
||||||
|
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
|
||||||
|
std::cerr << "GpuContext: SDL_ClaimWindowForGPUDevice failed: " << SDL_GetError() << '\n';
|
||||||
|
SDL_DestroyGPUDevice(device_);
|
||||||
|
device_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query swapchain format (Metal: typically B8G8R8A8_UNORM or R8G8B8A8_UNORM)
|
||||||
|
swapchain_format_ = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||||
|
std::cout << "GpuContext: swapchain format = " << static_cast<int>(swapchain_format_) << '\n';
|
||||||
|
|
||||||
|
// Default: VSync ON
|
||||||
|
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, SDL_GPU_PRESENTMODE_VSYNC);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuContext::destroy() {
|
||||||
|
if (device_ != nullptr) {
|
||||||
|
SDL_WaitForGPUIdle(device_);
|
||||||
|
SDL_ReleaseWindowFromGPUDevice(device_, window_);
|
||||||
|
SDL_DestroyGPUDevice(device_);
|
||||||
|
device_ = nullptr;
|
||||||
|
}
|
||||||
|
window_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuContext::acquireCommandBuffer() -> SDL_GPUCommandBuffer* {
|
||||||
|
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
|
||||||
|
if (cmd == nullptr) {
|
||||||
|
SDL_Log("GpuContext: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError());
|
||||||
|
}
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuContext::acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf,
|
||||||
|
Uint32* out_w,
|
||||||
|
Uint32* out_h) -> SDL_GPUTexture* {
|
||||||
|
SDL_GPUTexture* tex = nullptr;
|
||||||
|
if (!SDL_AcquireGPUSwapchainTexture(cmd_buf, window_, &tex, out_w, out_h)) {
|
||||||
|
SDL_Log("GpuContext: SDL_AcquireGPUSwapchainTexture failed: %s", SDL_GetError());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
// tex == nullptr when window is minimized — caller should skip rendering
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuContext::submit(SDL_GPUCommandBuffer* cmd_buf) {
|
||||||
|
SDL_SubmitGPUCommandBuffer(cmd_buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuContext::setVSync(bool enabled) -> bool {
|
||||||
|
SDL_GPUPresentMode mode = enabled ? SDL_GPU_PRESENTMODE_VSYNC
|
||||||
|
: SDL_GPU_PRESENTMODE_IMMEDIATE;
|
||||||
|
return SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, mode);
|
||||||
|
}
|
||||||
34
source/gpu/gpu_context.hpp
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
#include <SDL3/SDL_video.h>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GpuContext — SDL_GPU device + swapchain wrapper
|
||||||
|
// Replaces SDL_Renderer as the main rendering backend.
|
||||||
|
// ============================================================================
|
||||||
|
class GpuContext {
|
||||||
|
public:
|
||||||
|
bool init(SDL_Window* window);
|
||||||
|
void destroy();
|
||||||
|
|
||||||
|
SDL_GPUDevice* device() const { return device_; }
|
||||||
|
SDL_Window* window() const { return window_; }
|
||||||
|
SDL_GPUTextureFormat swapchainFormat() const { return swapchain_format_; }
|
||||||
|
|
||||||
|
// Per-frame helpers
|
||||||
|
SDL_GPUCommandBuffer* acquireCommandBuffer();
|
||||||
|
// Returns nullptr if window is minimized (swapchain not available).
|
||||||
|
SDL_GPUTexture* acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf,
|
||||||
|
Uint32* out_w,
|
||||||
|
Uint32* out_h);
|
||||||
|
static void submit(SDL_GPUCommandBuffer* cmd_buf);
|
||||||
|
|
||||||
|
// VSync control (call after init)
|
||||||
|
bool setVSync(bool enabled);
|
||||||
|
|
||||||
|
private:
|
||||||
|
SDL_GPUDevice* device_ = nullptr;
|
||||||
|
SDL_Window* window_ = nullptr;
|
||||||
|
SDL_GPUTextureFormat swapchain_format_ = SDL_GPU_TEXTUREFORMAT_INVALID;
|
||||||
|
};
|
||||||
508
source/gpu/gpu_pipeline.cpp
Normal file
@@ -0,0 +1,508 @@
|
|||||||
|
#include "gpu_pipeline.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_log.h>
|
||||||
|
|
||||||
|
#include <array> // for std::array
|
||||||
|
#include <cstddef> // offsetof
|
||||||
|
#include <cstring> // strlen
|
||||||
|
|
||||||
|
#include "gpu_ball_buffer.hpp" // for BallGPUData layout
|
||||||
|
#include "gpu_sprite_batch.hpp" // for GpuVertex layout
|
||||||
|
|
||||||
|
#ifndef __APPLE__
|
||||||
|
// Generated at build time by CMake + glslc (see cmake/spv_to_header.cmake)
|
||||||
|
#include "ball_vert_spv.h"
|
||||||
|
#include "postfx_frag_spv.h"
|
||||||
|
#include "postfx_vert_spv.h"
|
||||||
|
#include "sprite_frag_spv.h"
|
||||||
|
#include "sprite_vert_spv.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
// ============================================================================
|
||||||
|
// MSL Shaders (Metal Shading Language, macOS)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Sprite vertex shader
|
||||||
|
// Input: GpuVertex (pos=NDC float2, uv float2, col float4)
|
||||||
|
// Output: position, uv, col forwarded to fragment stage
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static const char* kSpriteVertMSL = R"(
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct SpriteVIn {
|
||||||
|
float2 pos [[attribute(0)]];
|
||||||
|
float2 uv [[attribute(1)]];
|
||||||
|
float4 col [[attribute(2)]];
|
||||||
|
};
|
||||||
|
struct SpriteVOut {
|
||||||
|
float4 pos [[position]];
|
||||||
|
float2 uv;
|
||||||
|
float4 col;
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex SpriteVOut sprite_vs(SpriteVIn in [[stage_in]]) {
|
||||||
|
SpriteVOut out;
|
||||||
|
out.pos = float4(in.pos, 0.0, 1.0);
|
||||||
|
out.uv = in.uv;
|
||||||
|
out.col = in.col;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Sprite fragment shader
|
||||||
|
// Samples a texture and multiplies by vertex color (for tinting + alpha).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static const char* kSpriteFragMSL = R"(
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct SpriteVOut {
|
||||||
|
float4 pos [[position]];
|
||||||
|
float2 uv;
|
||||||
|
float4 col;
|
||||||
|
};
|
||||||
|
|
||||||
|
fragment float4 sprite_fs(SpriteVOut in [[stage_in]],
|
||||||
|
texture2d<float> tex [[texture(0)]],
|
||||||
|
sampler samp [[sampler(0)]]) {
|
||||||
|
float4 t = tex.sample(samp, in.uv);
|
||||||
|
return float4(t.rgb * in.col.rgb, t.a * in.col.a);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PostFX vertex shader
|
||||||
|
// Generates a full-screen triangle from vertex_id (no vertex buffer needed).
|
||||||
|
// UV mapping: NDC(-1,-1)→UV(0,1) NDC(-1,3)→UV(0,-1) NDC(3,-1)→UV(2,1)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static const char* kPostFXVertMSL = R"(
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct PostVOut {
|
||||||
|
float4 pos [[position]];
|
||||||
|
float2 uv;
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex PostVOut postfx_vs(uint vid [[vertex_id]]) {
|
||||||
|
const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} };
|
||||||
|
const float2 uvs[3] = { { 0.0, 1.0}, {2.0, 1.0}, { 0.0,-1.0} };
|
||||||
|
PostVOut out;
|
||||||
|
out.pos = float4(positions[vid], 0.0, 1.0);
|
||||||
|
out.uv = uvs[vid];
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PostFX fragment shader
|
||||||
|
// Effects driven by PostFXUniforms (uniform buffer slot 0):
|
||||||
|
// - Chromatic aberration: RGB channel UV offset
|
||||||
|
// - Scanlines: sin-wave intensity modulation
|
||||||
|
// - Vignette: radial edge darkening
|
||||||
|
// MSL binding for fragment uniform buffer 0 with 1 sampler, 0 storage:
|
||||||
|
// constant PostFXUniforms& u [[buffer(0)]]
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static const char* kPostFXFragMSL = R"(
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct PostVOut {
|
||||||
|
float4 pos [[position]];
|
||||||
|
float2 uv;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PostFXUniforms {
|
||||||
|
float vignette_strength;
|
||||||
|
float chroma_strength;
|
||||||
|
float scanline_strength;
|
||||||
|
float screen_height;
|
||||||
|
};
|
||||||
|
|
||||||
|
fragment float4 postfx_fs(PostVOut in [[stage_in]],
|
||||||
|
texture2d<float> scene [[texture(0)]],
|
||||||
|
sampler samp [[sampler(0)]],
|
||||||
|
constant PostFXUniforms& u [[buffer(0)]]) {
|
||||||
|
// Chromatic aberration: offset R and B channels horizontally
|
||||||
|
float ca = u.chroma_strength * 0.005;
|
||||||
|
float4 color;
|
||||||
|
color.r = scene.sample(samp, in.uv + float2( ca, 0.0)).r;
|
||||||
|
color.g = scene.sample(samp, in.uv ).g;
|
||||||
|
color.b = scene.sample(samp, in.uv - float2( ca, 0.0)).b;
|
||||||
|
color.a = scene.sample(samp, in.uv ).a;
|
||||||
|
|
||||||
|
// Scanlines: horizontal sine-wave at ~360 lines (one dark band per 2 px at 720p)
|
||||||
|
float scan = 0.85 + 0.15 * sin(in.uv.y * 3.14159265 * u.screen_height);
|
||||||
|
color.rgb *= mix(1.0, scan, u.scanline_strength);
|
||||||
|
|
||||||
|
// Vignette: radial edge darkening
|
||||||
|
float2 d = in.uv - float2(0.5, 0.5);
|
||||||
|
float vignette = 1.0 - dot(d, d) * u.vignette_strength;
|
||||||
|
color.rgb *= clamp(vignette, 0.0, 1.0);
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Ball instanced vertex shader
|
||||||
|
// Reads BallGPUData as per-instance attributes (input_rate = INSTANCE).
|
||||||
|
// Generates a 6-vertex quad (2 triangles) per instance using vertex_id.
|
||||||
|
//
|
||||||
|
// BallGPUData layout:
|
||||||
|
// float2 center [[attribute(0)]] — NDC center (cx, cy)
|
||||||
|
// float2 half [[attribute(1)]] — NDC half-size (hw, hh), both positive
|
||||||
|
// float4 col [[attribute(2)]] — RGBA [0,1]
|
||||||
|
//
|
||||||
|
// NDC convention (SDL / Metal): Y increases upward (+1=top, -1=bottom).
|
||||||
|
// half.x = w/screen_w, half.y = h/screen_h (positive; Y is not flipped)
|
||||||
|
// Vertex order: TL TR BL | TR BR BL (CCW winding, standard Metal)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static const char* kBallInstancedVertMSL = R"(
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct BallInstance {
|
||||||
|
float2 center [[attribute(0)]]; // NDC center
|
||||||
|
float2 halfsize [[attribute(1)]]; // NDC half-size (both positive); 'half' is reserved in MSL
|
||||||
|
float4 col [[attribute(2)]];
|
||||||
|
};
|
||||||
|
struct BallVOut {
|
||||||
|
float4 pos [[position]];
|
||||||
|
float2 uv;
|
||||||
|
float4 col;
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex BallVOut ball_instanced_vs(BallInstance inst [[stage_in]],
|
||||||
|
uint vid [[vertex_id]]) {
|
||||||
|
// Offset signs for each of the 6 vertices (TL TR BL | TR BR BL)
|
||||||
|
const float2 offsets[6] = {
|
||||||
|
{-1.0f, 1.0f}, // TL
|
||||||
|
{ 1.0f, 1.0f}, // TR
|
||||||
|
{-1.0f, -1.0f}, // BL
|
||||||
|
{ 1.0f, 1.0f}, // TR (shared)
|
||||||
|
{ 1.0f, -1.0f}, // BR
|
||||||
|
{-1.0f, -1.0f}, // BL (shared)
|
||||||
|
};
|
||||||
|
// UV: TL=(0,0) TR=(1,0) BL=(0,1) BR=(1,1)
|
||||||
|
const float2 uvs[6] = {
|
||||||
|
{0.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 1.0f},
|
||||||
|
{1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f},
|
||||||
|
};
|
||||||
|
float2 pos = inst.center + offsets[vid] * inst.halfsize;
|
||||||
|
BallVOut out;
|
||||||
|
out.pos = float4(pos.x, pos.y, 0.0f, 1.0f);
|
||||||
|
out.uv = uvs[vid];
|
||||||
|
out.col = inst.col;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
#endif // __APPLE__
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GpuPipeline implementation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
auto GpuPipeline::init(SDL_GPUDevice* device,
|
||||||
|
SDL_GPUTextureFormat target_format,
|
||||||
|
SDL_GPUTextureFormat offscreen_format) -> bool {
|
||||||
|
SDL_GPUShaderFormat supported = SDL_GetGPUShaderFormats(device);
|
||||||
|
#ifdef __APPLE__
|
||||||
|
if (!(supported & SDL_GPU_SHADERFORMAT_MSL)) {
|
||||||
|
SDL_Log("GpuPipeline: MSL not supported (format mask=%u)", supported);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if ((supported & SDL_GPU_SHADERFORMAT_SPIRV) == 0u) {
|
||||||
|
SDL_Log("GpuPipeline: SPIRV not supported (format mask=%u)", supported);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Sprite pipeline
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
#ifdef __APPLE__
|
||||||
|
SDL_GPUShader* sprite_vert = createShader(device, kSpriteVertMSL, "sprite_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||||
|
SDL_GPUShader* sprite_frag = createShader(device, kSpriteFragMSL, "sprite_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
|
||||||
|
#else
|
||||||
|
SDL_GPUShader* sprite_vert = createShaderSPIRV(device, ksprite_vert_spv, ksprite_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||||
|
SDL_GPUShader* sprite_frag = createShaderSPIRV(device, ksprite_frag_spv, ksprite_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
|
||||||
|
#endif
|
||||||
|
if ((sprite_vert == nullptr) || (sprite_frag == nullptr)) {
|
||||||
|
SDL_Log("GpuPipeline: failed to create sprite shaders");
|
||||||
|
if (sprite_vert != nullptr) {
|
||||||
|
SDL_ReleaseGPUShader(device, sprite_vert);
|
||||||
|
}
|
||||||
|
if (sprite_frag != nullptr) {
|
||||||
|
SDL_ReleaseGPUShader(device, sprite_frag);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertex input: GpuVertex layout
|
||||||
|
SDL_GPUVertexBufferDescription vb_desc = {};
|
||||||
|
vb_desc.slot = 0;
|
||||||
|
vb_desc.pitch = sizeof(GpuVertex);
|
||||||
|
vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;
|
||||||
|
vb_desc.instance_step_rate = 0;
|
||||||
|
|
||||||
|
std::array<SDL_GPUVertexAttribute, 3> attrs = {};
|
||||||
|
attrs[0].location = 0;
|
||||||
|
attrs[0].buffer_slot = 0;
|
||||||
|
attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
||||||
|
attrs[0].offset = static_cast<Uint32>(offsetof(GpuVertex, x));
|
||||||
|
|
||||||
|
attrs[1].location = 1;
|
||||||
|
attrs[1].buffer_slot = 0;
|
||||||
|
attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
||||||
|
attrs[1].offset = static_cast<Uint32>(offsetof(GpuVertex, u));
|
||||||
|
|
||||||
|
attrs[2].location = 2;
|
||||||
|
attrs[2].buffer_slot = 0;
|
||||||
|
attrs[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
|
||||||
|
attrs[2].offset = static_cast<Uint32>(offsetof(GpuVertex, r));
|
||||||
|
|
||||||
|
SDL_GPUVertexInputState vertex_input = {};
|
||||||
|
vertex_input.vertex_buffer_descriptions = &vb_desc;
|
||||||
|
vertex_input.num_vertex_buffers = 1;
|
||||||
|
vertex_input.vertex_attributes = attrs.data();
|
||||||
|
vertex_input.num_vertex_attributes = 3;
|
||||||
|
|
||||||
|
// Alpha blend state (SRC_ALPHA, ONE_MINUS_SRC_ALPHA)
|
||||||
|
SDL_GPUColorTargetBlendState blend = {};
|
||||||
|
blend.enable_blend = true;
|
||||||
|
blend.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA;
|
||||||
|
blend.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
|
||||||
|
blend.color_blend_op = SDL_GPU_BLENDOP_ADD;
|
||||||
|
blend.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE;
|
||||||
|
blend.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
|
||||||
|
blend.alpha_blend_op = SDL_GPU_BLENDOP_ADD;
|
||||||
|
blend.enable_color_write_mask = false; // write all channels
|
||||||
|
|
||||||
|
SDL_GPUColorTargetDescription color_target_desc = {};
|
||||||
|
color_target_desc.format = offscreen_format;
|
||||||
|
color_target_desc.blend_state = blend;
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipelineCreateInfo sprite_pipe_info = {};
|
||||||
|
sprite_pipe_info.vertex_shader = sprite_vert;
|
||||||
|
sprite_pipe_info.fragment_shader = sprite_frag;
|
||||||
|
sprite_pipe_info.vertex_input_state = vertex_input;
|
||||||
|
sprite_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||||
|
sprite_pipe_info.target_info.num_color_targets = 1;
|
||||||
|
sprite_pipe_info.target_info.color_target_descriptions = &color_target_desc;
|
||||||
|
|
||||||
|
sprite_pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &sprite_pipe_info);
|
||||||
|
|
||||||
|
SDL_ReleaseGPUShader(device, sprite_vert);
|
||||||
|
SDL_ReleaseGPUShader(device, sprite_frag);
|
||||||
|
|
||||||
|
if (sprite_pipeline_ == nullptr) {
|
||||||
|
SDL_Log("GpuPipeline: sprite pipeline creation failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Ball instanced pipeline
|
||||||
|
// Vertex: ball_instanced_vs (BallGPUData per-instance, no index buffer)
|
||||||
|
// Fragment: sprite_fs (same texture+color blend as sprite pipeline)
|
||||||
|
// Targets: offscreen (same as sprite pipeline)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
#ifdef __APPLE__
|
||||||
|
SDL_GPUShader* ball_vert = createShader(device, kBallInstancedVertMSL, "ball_instanced_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||||
|
SDL_GPUShader* ball_frag = createShader(device, kSpriteFragMSL, "sprite_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
|
||||||
|
#else
|
||||||
|
SDL_GPUShader* ball_vert = createShaderSPIRV(device, kball_vert_spv, kball_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||||
|
SDL_GPUShader* ball_frag = createShaderSPIRV(device, ksprite_frag_spv, ksprite_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
|
||||||
|
#endif
|
||||||
|
if ((ball_vert == nullptr) || (ball_frag == nullptr)) {
|
||||||
|
SDL_Log("GpuPipeline: failed to create ball instanced shaders");
|
||||||
|
if (ball_vert != nullptr) {
|
||||||
|
SDL_ReleaseGPUShader(device, ball_vert);
|
||||||
|
}
|
||||||
|
if (ball_frag != nullptr) {
|
||||||
|
SDL_ReleaseGPUShader(device, ball_frag);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertex input: BallGPUData as per-instance data (step rate = 1 instance)
|
||||||
|
SDL_GPUVertexBufferDescription ball_vb_desc = {};
|
||||||
|
ball_vb_desc.slot = 0;
|
||||||
|
ball_vb_desc.pitch = sizeof(BallGPUData);
|
||||||
|
ball_vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_INSTANCE;
|
||||||
|
ball_vb_desc.instance_step_rate = 1;
|
||||||
|
|
||||||
|
std::array<SDL_GPUVertexAttribute, 3> ball_attrs = {};
|
||||||
|
// attr 0: center (float2) at offset 0
|
||||||
|
ball_attrs[0].location = 0;
|
||||||
|
ball_attrs[0].buffer_slot = 0;
|
||||||
|
ball_attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
||||||
|
ball_attrs[0].offset = static_cast<Uint32>(offsetof(BallGPUData, cx));
|
||||||
|
// attr 1: half-size (float2) at offset 8
|
||||||
|
ball_attrs[1].location = 1;
|
||||||
|
ball_attrs[1].buffer_slot = 0;
|
||||||
|
ball_attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
||||||
|
ball_attrs[1].offset = static_cast<Uint32>(offsetof(BallGPUData, hw));
|
||||||
|
// attr 2: color (float4) at offset 16
|
||||||
|
ball_attrs[2].location = 2;
|
||||||
|
ball_attrs[2].buffer_slot = 0;
|
||||||
|
ball_attrs[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
|
||||||
|
ball_attrs[2].offset = static_cast<Uint32>(offsetof(BallGPUData, r));
|
||||||
|
|
||||||
|
SDL_GPUVertexInputState ball_vertex_input = {};
|
||||||
|
ball_vertex_input.vertex_buffer_descriptions = &ball_vb_desc;
|
||||||
|
ball_vertex_input.num_vertex_buffers = 1;
|
||||||
|
ball_vertex_input.vertex_attributes = ball_attrs.data();
|
||||||
|
ball_vertex_input.num_vertex_attributes = 3;
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipelineCreateInfo ball_pipe_info = {};
|
||||||
|
ball_pipe_info.vertex_shader = ball_vert;
|
||||||
|
ball_pipe_info.fragment_shader = ball_frag;
|
||||||
|
ball_pipe_info.vertex_input_state = ball_vertex_input;
|
||||||
|
ball_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||||
|
ball_pipe_info.target_info.num_color_targets = 1;
|
||||||
|
ball_pipe_info.target_info.color_target_descriptions = &color_target_desc;
|
||||||
|
|
||||||
|
ball_pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &ball_pipe_info);
|
||||||
|
|
||||||
|
SDL_ReleaseGPUShader(device, ball_vert);
|
||||||
|
SDL_ReleaseGPUShader(device, ball_frag);
|
||||||
|
|
||||||
|
if (ball_pipeline_ == nullptr) {
|
||||||
|
SDL_Log("GpuPipeline: ball instanced pipeline creation failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// UI overlay pipeline (same as sprite but renders to swapchain format)
|
||||||
|
// Reuse sprite shaders with different target format.
|
||||||
|
// We create a second version of the sprite pipeline for swapchain.
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// (postfx pipeline targets swapchain; UI overlay also targets swapchain
|
||||||
|
// but needs its own pipeline with swapchain format.)
|
||||||
|
// For simplicity, the sprite pipeline is used for the offscreen pass only.
|
||||||
|
// The UI overlay is composited via a separate postfx-like pass below.
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// PostFX pipeline
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
#ifdef __APPLE__
|
||||||
|
SDL_GPUShader* postfx_vert = createShader(device, kPostFXVertMSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||||
|
SDL_GPUShader* postfx_frag = createShader(device, kPostFXFragMSL, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||||
|
#else
|
||||||
|
SDL_GPUShader* postfx_vert = createShaderSPIRV(device, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||||
|
SDL_GPUShader* postfx_frag = createShaderSPIRV(device, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||||
|
#endif
|
||||||
|
if ((postfx_vert == nullptr) || (postfx_frag == nullptr)) {
|
||||||
|
SDL_Log("GpuPipeline: failed to create postfx shaders");
|
||||||
|
if (postfx_vert != nullptr) {
|
||||||
|
SDL_ReleaseGPUShader(device, postfx_vert);
|
||||||
|
}
|
||||||
|
if (postfx_frag != nullptr) {
|
||||||
|
SDL_ReleaseGPUShader(device, postfx_frag);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostFX: no vertex input (uses vertex_id), no blend (replace output)
|
||||||
|
SDL_GPUColorTargetBlendState no_blend = {};
|
||||||
|
no_blend.enable_blend = false;
|
||||||
|
no_blend.enable_color_write_mask = false;
|
||||||
|
|
||||||
|
SDL_GPUColorTargetDescription postfx_target_desc = {};
|
||||||
|
postfx_target_desc.format = target_format;
|
||||||
|
postfx_target_desc.blend_state = no_blend;
|
||||||
|
|
||||||
|
SDL_GPUVertexInputState no_input = {};
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipelineCreateInfo postfx_pipe_info = {};
|
||||||
|
postfx_pipe_info.vertex_shader = postfx_vert;
|
||||||
|
postfx_pipe_info.fragment_shader = postfx_frag;
|
||||||
|
postfx_pipe_info.vertex_input_state = no_input;
|
||||||
|
postfx_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||||
|
postfx_pipe_info.target_info.num_color_targets = 1;
|
||||||
|
postfx_pipe_info.target_info.color_target_descriptions = &postfx_target_desc;
|
||||||
|
|
||||||
|
postfx_pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &postfx_pipe_info);
|
||||||
|
|
||||||
|
SDL_ReleaseGPUShader(device, postfx_vert);
|
||||||
|
SDL_ReleaseGPUShader(device, postfx_frag);
|
||||||
|
|
||||||
|
if (postfx_pipeline_ == nullptr) {
|
||||||
|
SDL_Log("GpuPipeline: postfx pipeline creation failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Log("GpuPipeline: all pipelines created successfully");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuPipeline::destroy(SDL_GPUDevice* device) {
|
||||||
|
if (sprite_pipeline_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUGraphicsPipeline(device, sprite_pipeline_);
|
||||||
|
sprite_pipeline_ = nullptr;
|
||||||
|
}
|
||||||
|
if (ball_pipeline_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUGraphicsPipeline(device, ball_pipeline_);
|
||||||
|
ball_pipeline_ = nullptr;
|
||||||
|
}
|
||||||
|
if (postfx_pipeline_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUGraphicsPipeline(device, postfx_pipeline_);
|
||||||
|
postfx_pipeline_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuPipeline::createShaderSPIRV(SDL_GPUDevice* device,
|
||||||
|
const uint8_t* spv_code,
|
||||||
|
size_t spv_size,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers,
|
||||||
|
Uint32 num_storage_buffers) -> SDL_GPUShader* {
|
||||||
|
SDL_GPUShaderCreateInfo info = {};
|
||||||
|
info.code = spv_code;
|
||||||
|
info.code_size = spv_size;
|
||||||
|
info.entrypoint = entrypoint;
|
||||||
|
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||||
|
info.stage = stage;
|
||||||
|
info.num_samplers = num_samplers;
|
||||||
|
info.num_storage_textures = 0;
|
||||||
|
info.num_storage_buffers = num_storage_buffers;
|
||||||
|
info.num_uniform_buffers = num_uniform_buffers;
|
||||||
|
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
|
||||||
|
if (shader == nullptr) {
|
||||||
|
SDL_Log("GpuPipeline: SPIRV shader '%s' failed: %s", entrypoint, SDL_GetError());
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuPipeline::createShader(SDL_GPUDevice* device,
|
||||||
|
const char* msl_source,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers,
|
||||||
|
Uint32 num_storage_buffers) -> SDL_GPUShader* {
|
||||||
|
SDL_GPUShaderCreateInfo info = {};
|
||||||
|
info.code = reinterpret_cast<const Uint8*>(msl_source);
|
||||||
|
info.code_size = static_cast<size_t>(strlen(msl_source) + 1);
|
||||||
|
info.entrypoint = entrypoint;
|
||||||
|
info.format = SDL_GPU_SHADERFORMAT_MSL;
|
||||||
|
info.stage = stage;
|
||||||
|
info.num_samplers = num_samplers;
|
||||||
|
info.num_storage_textures = 0;
|
||||||
|
info.num_storage_buffers = num_storage_buffers;
|
||||||
|
info.num_uniform_buffers = num_uniform_buffers;
|
||||||
|
|
||||||
|
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
|
||||||
|
if (shader == nullptr) {
|
||||||
|
SDL_Log("GpuPipeline: shader '%s' failed: %s", entrypoint, SDL_GetError());
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
63
source/gpu/gpu_pipeline.hpp
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// PostFXUniforms — pushed to the fragment stage each frame via
|
||||||
|
// SDL_PushGPUFragmentUniformData(pass, 0, &uniforms, sizeof(PostFXUniforms))
|
||||||
|
// MSL binding: constant PostFXUniforms& u [[buffer(0)]]
|
||||||
|
// ============================================================================
|
||||||
|
struct PostFXUniforms {
|
||||||
|
float vignette_strength; // 0 = none, 0.8 = default subtle
|
||||||
|
float chroma_strength; // 0 = off, 0.2 = default chromatic aberration
|
||||||
|
float scanline_strength; // 0 = off, 1 = full scanlines
|
||||||
|
float screen_height; // logical render target height (px), for resolution-independent scanlines
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GpuPipeline — Creates and owns the graphics pipelines used by the engine.
|
||||||
|
//
|
||||||
|
// sprite_pipeline_ : textured quads, alpha blending.
|
||||||
|
// Vertex layout: GpuVertex (pos float2, uv float2, col float4).
|
||||||
|
// ball_pipeline_ : instanced ball rendering, alpha blending.
|
||||||
|
// Vertex layout: BallGPUData as per-instance data (input_rate=INSTANCE).
|
||||||
|
// 6 procedural vertices per instance (no index buffer).
|
||||||
|
// postfx_pipeline_ : full-screen triangle, no vertex buffer, no blend.
|
||||||
|
// Reads offscreen texture, writes to swapchain.
|
||||||
|
// Accepts PostFXUniforms via fragment uniform buffer slot 0.
|
||||||
|
// ============================================================================
|
||||||
|
class GpuPipeline {
|
||||||
|
public:
|
||||||
|
// target_format: pass SDL_GetGPUSwapchainTextureFormat() result.
|
||||||
|
// offscreen_format: format of the offscreen render target.
|
||||||
|
bool init(SDL_GPUDevice* device,
|
||||||
|
SDL_GPUTextureFormat target_format,
|
||||||
|
SDL_GPUTextureFormat offscreen_format);
|
||||||
|
void destroy(SDL_GPUDevice* device);
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipeline* spritePipeline() const { return sprite_pipeline_; }
|
||||||
|
SDL_GPUGraphicsPipeline* ballPipeline() const { return ball_pipeline_; }
|
||||||
|
SDL_GPUGraphicsPipeline* postfxPipeline() const { return postfx_pipeline_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static SDL_GPUShader* createShader(SDL_GPUDevice* device,
|
||||||
|
const char* msl_source,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers,
|
||||||
|
Uint32 num_storage_buffers = 0);
|
||||||
|
|
||||||
|
static SDL_GPUShader* createShaderSPIRV(SDL_GPUDevice* device,
|
||||||
|
const uint8_t* spv_code,
|
||||||
|
size_t spv_size,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers,
|
||||||
|
Uint32 num_storage_buffers = 0);
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipeline* sprite_pipeline_ = nullptr;
|
||||||
|
SDL_GPUGraphicsPipeline* ball_pipeline_ = nullptr;
|
||||||
|
SDL_GPUGraphicsPipeline* postfx_pipeline_ = nullptr;
|
||||||
|
};
|
||||||
236
source/gpu/gpu_sprite_batch.cpp
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
#include "gpu_sprite_batch.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_log.h>
|
||||||
|
|
||||||
|
#include <cstring> // memcpy
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public interface
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
auto GpuSpriteBatch::init(SDL_GPUDevice* device, int max_sprites) -> bool {
|
||||||
|
max_sprites_ = max_sprites;
|
||||||
|
// Pre-allocate GPU buffers large enough for (max_sprites_ + 2) quads.
|
||||||
|
// The +2 reserves one slot for the background quad and one for the fullscreen overlay.
|
||||||
|
Uint32 max_verts = static_cast<Uint32>(max_sprites_ + 2) * 4;
|
||||||
|
Uint32 max_indices = static_cast<Uint32>(max_sprites_ + 2) * 6;
|
||||||
|
|
||||||
|
Uint32 vb_size = max_verts * sizeof(GpuVertex);
|
||||||
|
Uint32 ib_size = max_indices * sizeof(uint32_t);
|
||||||
|
|
||||||
|
// Vertex buffer
|
||||||
|
SDL_GPUBufferCreateInfo vb_info = {};
|
||||||
|
vb_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
|
||||||
|
vb_info.size = vb_size;
|
||||||
|
vertex_buf_ = SDL_CreateGPUBuffer(device, &vb_info);
|
||||||
|
if (vertex_buf_ == nullptr) {
|
||||||
|
SDL_Log("GpuSpriteBatch: vertex buffer creation failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index buffer
|
||||||
|
SDL_GPUBufferCreateInfo ib_info = {};
|
||||||
|
ib_info.usage = SDL_GPU_BUFFERUSAGE_INDEX;
|
||||||
|
ib_info.size = ib_size;
|
||||||
|
index_buf_ = SDL_CreateGPUBuffer(device, &ib_info);
|
||||||
|
if (index_buf_ == nullptr) {
|
||||||
|
SDL_Log("GpuSpriteBatch: index buffer creation failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfer buffers (reused every frame via cycle=true on upload)
|
||||||
|
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||||
|
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||||
|
|
||||||
|
tb_info.size = vb_size;
|
||||||
|
vertex_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
|
||||||
|
if (vertex_transfer_ == nullptr) {
|
||||||
|
SDL_Log("GpuSpriteBatch: vertex transfer buffer failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
tb_info.size = ib_size;
|
||||||
|
index_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
|
||||||
|
if (index_transfer_ == nullptr) {
|
||||||
|
SDL_Log("GpuSpriteBatch: index transfer buffer failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
vertices_.reserve(static_cast<size_t>(max_sprites_ + 2) * 4);
|
||||||
|
indices_.reserve(static_cast<size_t>(max_sprites_ + 2) * 6);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuSpriteBatch::destroy(SDL_GPUDevice* device) {
|
||||||
|
if (device == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (vertex_transfer_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTransferBuffer(device, vertex_transfer_);
|
||||||
|
vertex_transfer_ = nullptr;
|
||||||
|
}
|
||||||
|
if (index_transfer_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTransferBuffer(device, index_transfer_);
|
||||||
|
index_transfer_ = nullptr;
|
||||||
|
}
|
||||||
|
if (vertex_buf_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUBuffer(device, vertex_buf_);
|
||||||
|
vertex_buf_ = nullptr;
|
||||||
|
}
|
||||||
|
if (index_buf_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUBuffer(device, index_buf_);
|
||||||
|
index_buf_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuSpriteBatch::beginFrame() {
|
||||||
|
vertices_.clear();
|
||||||
|
indices_.clear();
|
||||||
|
bg_index_count_ = 0;
|
||||||
|
sprite_index_offset_ = 0;
|
||||||
|
sprite_index_count_ = 0;
|
||||||
|
overlay_index_offset_ = 0;
|
||||||
|
overlay_index_count_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuSpriteBatch::addBackground(float screen_w, float screen_h, float top_r, float top_g, float top_b, float bot_r, float bot_g, float bot_b) {
|
||||||
|
// Background is the full screen quad, corners:
|
||||||
|
// TL(-1, 1) TR(1, 1) → top color
|
||||||
|
// BL(-1,-1) BR(1,-1) → bottom color
|
||||||
|
// We push it as 4 separate vertices (different colors per row).
|
||||||
|
auto vi = static_cast<uint32_t>(vertices_.size());
|
||||||
|
|
||||||
|
// Top-left
|
||||||
|
vertices_.push_back({-1.0f, 1.0f, 0.0f, 0.0f, top_r, top_g, top_b, 1.0f});
|
||||||
|
// Top-right
|
||||||
|
vertices_.push_back({1.0f, 1.0f, 1.0f, 0.0f, top_r, top_g, top_b, 1.0f});
|
||||||
|
// Bottom-right
|
||||||
|
vertices_.push_back({1.0f, -1.0f, 1.0f, 1.0f, bot_r, bot_g, bot_b, 1.0f});
|
||||||
|
// Bottom-left
|
||||||
|
vertices_.push_back({-1.0f, -1.0f, 0.0f, 1.0f, bot_r, bot_g, bot_b, 1.0f});
|
||||||
|
|
||||||
|
// Two triangles: TL-TR-BR, BR-BL-TL
|
||||||
|
indices_.push_back(vi + 0);
|
||||||
|
indices_.push_back(vi + 1);
|
||||||
|
indices_.push_back(vi + 2);
|
||||||
|
indices_.push_back(vi + 2);
|
||||||
|
indices_.push_back(vi + 3);
|
||||||
|
indices_.push_back(vi + 0);
|
||||||
|
|
||||||
|
bg_index_count_ = 6;
|
||||||
|
sprite_index_offset_ = 6;
|
||||||
|
|
||||||
|
(void)screen_w;
|
||||||
|
(void)screen_h; // unused — bg always covers full NDC
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuSpriteBatch::addSprite(float x, float y, float w, float h, float r, float g, float b, float a, float scale, float screen_w, float screen_h) {
|
||||||
|
// Apply scale around the sprite centre
|
||||||
|
float scaled_w = w * scale;
|
||||||
|
float scaled_h = h * scale;
|
||||||
|
float offset_x = (w - scaled_w) * 0.5f;
|
||||||
|
float offset_y = (h - scaled_h) * 0.5f;
|
||||||
|
|
||||||
|
float px0 = x + offset_x;
|
||||||
|
float py0 = y + offset_y;
|
||||||
|
float px1 = px0 + scaled_w;
|
||||||
|
float py1 = py0 + scaled_h;
|
||||||
|
|
||||||
|
float ndx0;
|
||||||
|
float ndy0;
|
||||||
|
float ndx1;
|
||||||
|
float ndy1;
|
||||||
|
toNDC(px0, py0, screen_w, screen_h, ndx0, ndy0);
|
||||||
|
toNDC(px1, py1, screen_w, screen_h, ndx1, ndy1);
|
||||||
|
|
||||||
|
pushQuad(ndx0, ndy0, ndx1, ndy1, 0.0f, 0.0f, 1.0f, 1.0f, r, g, b, a);
|
||||||
|
sprite_index_count_ += 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuSpriteBatch::addFullscreenOverlay() {
|
||||||
|
// El overlay es un slot reservado fuera del espacio de max_sprites_, igual que el background.
|
||||||
|
// Escribe directamente sin pasar por el guard de pushQuad().
|
||||||
|
overlay_index_offset_ = static_cast<int>(indices_.size());
|
||||||
|
auto vi = static_cast<uint32_t>(vertices_.size());
|
||||||
|
vertices_.push_back({-1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f});
|
||||||
|
vertices_.push_back({1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f});
|
||||||
|
vertices_.push_back({1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f});
|
||||||
|
vertices_.push_back({-1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f});
|
||||||
|
indices_.push_back(vi + 0);
|
||||||
|
indices_.push_back(vi + 1);
|
||||||
|
indices_.push_back(vi + 2);
|
||||||
|
indices_.push_back(vi + 2);
|
||||||
|
indices_.push_back(vi + 3);
|
||||||
|
indices_.push_back(vi + 0);
|
||||||
|
overlay_index_count_ = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuSpriteBatch::uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf) -> bool {
|
||||||
|
if (vertices_.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto vb_size = static_cast<Uint32>(vertices_.size() * sizeof(GpuVertex));
|
||||||
|
auto ib_size = static_cast<Uint32>(indices_.size() * sizeof(uint32_t));
|
||||||
|
|
||||||
|
// Map → write → unmap transfer buffers
|
||||||
|
void* vp = SDL_MapGPUTransferBuffer(device, vertex_transfer_, true /* cycle */);
|
||||||
|
if (vp == nullptr) {
|
||||||
|
SDL_Log("GpuSpriteBatch: vertex map failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(vp, vertices_.data(), vb_size);
|
||||||
|
SDL_UnmapGPUTransferBuffer(device, vertex_transfer_);
|
||||||
|
|
||||||
|
void* ip = SDL_MapGPUTransferBuffer(device, index_transfer_, true /* cycle */);
|
||||||
|
if (ip == nullptr) {
|
||||||
|
SDL_Log("GpuSpriteBatch: index map failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(ip, indices_.data(), ib_size);
|
||||||
|
SDL_UnmapGPUTransferBuffer(device, index_transfer_);
|
||||||
|
|
||||||
|
// Upload via copy pass
|
||||||
|
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd_buf);
|
||||||
|
|
||||||
|
SDL_GPUTransferBufferLocation v_src = {vertex_transfer_, 0};
|
||||||
|
SDL_GPUBufferRegion v_dst = {vertex_buf_, 0, vb_size};
|
||||||
|
SDL_UploadToGPUBuffer(copy, &v_src, &v_dst, true /* cycle */);
|
||||||
|
|
||||||
|
SDL_GPUTransferBufferLocation i_src = {index_transfer_, 0};
|
||||||
|
SDL_GPUBufferRegion i_dst = {index_buf_, 0, ib_size};
|
||||||
|
SDL_UploadToGPUBuffer(copy, &i_src, &i_dst, true /* cycle */);
|
||||||
|
|
||||||
|
SDL_EndGPUCopyPass(copy);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Private helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void GpuSpriteBatch::toNDC(float px, float py, float screen_w, float screen_h, float& ndx, float& ndy) {
|
||||||
|
ndx = (px / screen_w) * 2.0f - 1.0f;
|
||||||
|
ndy = 1.0f - (py / screen_h) * 2.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuSpriteBatch::pushQuad(float ndx0, float ndy0, float ndx1, float ndy1, float u0, float v0, float u1, float v1, float r, float g, float b, float a) {
|
||||||
|
// +1 reserva el slot del background que ya entró sin pasar por este guard.
|
||||||
|
if (vertices_.size() + 4 > static_cast<size_t>(max_sprites_ + 1) * 4) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto vi = static_cast<uint32_t>(vertices_.size());
|
||||||
|
|
||||||
|
// TL, TR, BR, BL
|
||||||
|
vertices_.push_back({ndx0, ndy0, u0, v0, r, g, b, a});
|
||||||
|
vertices_.push_back({ndx1, ndy0, u1, v0, r, g, b, a});
|
||||||
|
vertices_.push_back({ndx1, ndy1, u1, v1, r, g, b, a});
|
||||||
|
vertices_.push_back({ndx0, ndy1, u0, v1, r, g, b, a});
|
||||||
|
|
||||||
|
indices_.push_back(vi + 0);
|
||||||
|
indices_.push_back(vi + 1);
|
||||||
|
indices_.push_back(vi + 2);
|
||||||
|
indices_.push_back(vi + 2);
|
||||||
|
indices_.push_back(vi + 3);
|
||||||
|
indices_.push_back(vi + 0);
|
||||||
|
}
|
||||||
81
source/gpu/gpu_sprite_batch.hpp
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// GpuVertex — 8-float vertex layout sent to the GPU.
|
||||||
|
// Position is in NDC (pre-transformed on CPU), UV in [0,1], color in [0,1].
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct GpuVertex {
|
||||||
|
float x, y; // NDC position (−1..1)
|
||||||
|
float u, v; // Texture coords (0..1)
|
||||||
|
float r, g, b, a; // RGBA color (0..1)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GpuSpriteBatch — Accumulates sprite quads, uploads them in one copy pass.
|
||||||
|
//
|
||||||
|
// Usage per frame:
|
||||||
|
// batch.beginFrame();
|
||||||
|
// batch.addBackground(...); // Must be first (bg indices = [0..5])
|
||||||
|
// batch.addSprite(...) × N;
|
||||||
|
// batch.uploadBatch(device, cmd); // Copy pass
|
||||||
|
// // Then in render pass: bind buffers, draw bg with white tex, draw sprites.
|
||||||
|
// ============================================================================
|
||||||
|
class GpuSpriteBatch {
|
||||||
|
public:
|
||||||
|
// Default maximum sprites (background + UI overlay each count as one sprite)
|
||||||
|
static constexpr int DEFAULT_MAX_SPRITES = 200000;
|
||||||
|
|
||||||
|
bool init(SDL_GPUDevice* device, int max_sprites = DEFAULT_MAX_SPRITES);
|
||||||
|
void destroy(SDL_GPUDevice* device);
|
||||||
|
|
||||||
|
void beginFrame();
|
||||||
|
|
||||||
|
// Add the full-screen background gradient quad.
|
||||||
|
// top_* and bot_* are RGB in [0,1].
|
||||||
|
void addBackground(float screen_w, float screen_h, float top_r, float top_g, float top_b, float bot_r, float bot_g, float bot_b);
|
||||||
|
|
||||||
|
// Add a sprite quad (pixel coordinates).
|
||||||
|
// scale: uniform scale around the quad centre.
|
||||||
|
void addSprite(float x, float y, float w, float h, float r, float g, float b, float a, float scale, float screen_w, float screen_h);
|
||||||
|
|
||||||
|
// Add a full-screen overlay quad (e.g. UI surface, NDC −1..1).
|
||||||
|
void addFullscreenOverlay();
|
||||||
|
|
||||||
|
// Upload CPU vectors to GPU buffers via a copy pass.
|
||||||
|
// Returns false if the batch is empty.
|
||||||
|
bool uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf);
|
||||||
|
|
||||||
|
SDL_GPUBuffer* vertexBuffer() const { return vertex_buf_; }
|
||||||
|
SDL_GPUBuffer* indexBuffer() const { return index_buf_; }
|
||||||
|
int bgIndexCount() const { return bg_index_count_; }
|
||||||
|
int overlayIndexOffset() const { return overlay_index_offset_; }
|
||||||
|
int overlayIndexCount() const { return overlay_index_count_; }
|
||||||
|
int spriteIndexOffset() const { return sprite_index_offset_; }
|
||||||
|
int spriteIndexCount() const { return sprite_index_count_; }
|
||||||
|
bool isEmpty() const { return vertices_.empty(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void toNDC(float px, float py, float screen_w, float screen_h, float& ndx, float& ndy);
|
||||||
|
void pushQuad(float ndx0, float ndy0, float ndx1, float ndy1, float u0, float v0, float u1, float v1, float r, float g, float b, float a);
|
||||||
|
|
||||||
|
std::vector<GpuVertex> vertices_;
|
||||||
|
std::vector<uint32_t> indices_;
|
||||||
|
|
||||||
|
SDL_GPUBuffer* vertex_buf_ = nullptr;
|
||||||
|
SDL_GPUBuffer* index_buf_ = nullptr;
|
||||||
|
SDL_GPUTransferBuffer* vertex_transfer_ = nullptr;
|
||||||
|
SDL_GPUTransferBuffer* index_transfer_ = nullptr;
|
||||||
|
|
||||||
|
int bg_index_count_ = 0;
|
||||||
|
int sprite_index_offset_ = 0;
|
||||||
|
int sprite_index_count_ = 0;
|
||||||
|
int overlay_index_offset_ = 0;
|
||||||
|
int overlay_index_count_ = 0;
|
||||||
|
|
||||||
|
int max_sprites_ = DEFAULT_MAX_SPRITES;
|
||||||
|
};
|
||||||
228
source/gpu/gpu_texture.cpp
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
#include "gpu_texture.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_log.h>
|
||||||
|
#include <SDL3/SDL_pixels.h>
|
||||||
|
|
||||||
|
#include <array> // for std::array
|
||||||
|
#include <cstring> // memcpy
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// stb_image is compiled in texture.cpp (STB_IMAGE_IMPLEMENTATION defined there)
|
||||||
|
#include "external/stb_image.h"
|
||||||
|
#include "resource_manager.hpp"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public interface
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
auto GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) -> bool {
|
||||||
|
unsigned char* resource_data = nullptr;
|
||||||
|
size_t resource_size = 0;
|
||||||
|
|
||||||
|
if (!ResourceManager::loadResource(file_path, resource_data, resource_size)) {
|
||||||
|
SDL_Log("GpuTexture: can't load resource '%s'", file_path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int w = 0;
|
||||||
|
int h = 0;
|
||||||
|
int orig = 0;
|
||||||
|
unsigned char* pixels = stbi_load_from_memory(
|
||||||
|
resource_data,
|
||||||
|
static_cast<int>(resource_size),
|
||||||
|
&w,
|
||||||
|
&h,
|
||||||
|
&orig,
|
||||||
|
STBI_rgb_alpha);
|
||||||
|
delete[] resource_data;
|
||||||
|
|
||||||
|
if (pixels == nullptr) {
|
||||||
|
SDL_Log("GpuTexture: stbi decode failed for '%s': %s",
|
||||||
|
file_path.c_str(),
|
||||||
|
stbi_failure_reason());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(device);
|
||||||
|
bool ok = uploadPixels(device, pixels, w, h, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
|
||||||
|
stbi_image_free(pixels);
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
ok = createSampler(device, true /*nearest = pixel-perfect sprites*/);
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuTexture::fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest) -> bool {
|
||||||
|
if (surface == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure RGBA32 format
|
||||||
|
SDL_Surface* rgba = surface;
|
||||||
|
bool need_free = false;
|
||||||
|
if (surface->format != SDL_PIXELFORMAT_RGBA32) {
|
||||||
|
rgba = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
|
||||||
|
if (rgba == nullptr) {
|
||||||
|
SDL_Log("GpuTexture: SDL_ConvertSurface failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
need_free = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(device);
|
||||||
|
bool ok = uploadPixels(device, rgba->pixels, rgba->w, rgba->h, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
|
||||||
|
if (ok) {
|
||||||
|
ok = createSampler(device, nearest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (need_free) {
|
||||||
|
SDL_DestroySurface(rgba);
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuTexture::createRenderTarget(SDL_GPUDevice* device, int w, int h, SDL_GPUTextureFormat format) -> bool {
|
||||||
|
destroy(device);
|
||||||
|
|
||||||
|
SDL_GPUTextureCreateInfo info = {};
|
||||||
|
info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||||
|
info.format = format;
|
||||||
|
info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||||
|
info.width = static_cast<Uint32>(w);
|
||||||
|
info.height = static_cast<Uint32>(h);
|
||||||
|
info.layer_count_or_depth = 1;
|
||||||
|
info.num_levels = 1;
|
||||||
|
info.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||||
|
|
||||||
|
texture_ = SDL_CreateGPUTexture(device, &info);
|
||||||
|
if (texture_ == nullptr) {
|
||||||
|
SDL_Log("GpuTexture: createRenderTarget failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
width_ = w;
|
||||||
|
height_ = h;
|
||||||
|
|
||||||
|
// Render targets are sampled with linear filter (postfx reads them)
|
||||||
|
return createSampler(device, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuTexture::createWhite(SDL_GPUDevice* device) -> bool {
|
||||||
|
destroy(device);
|
||||||
|
// 1×1 white RGBA pixel
|
||||||
|
constexpr std::array<Uint8, 4> WHITE = {255, 255, 255, 255};
|
||||||
|
bool ok = uploadPixels(device, WHITE.data(), 1, 1, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
|
||||||
|
if (ok) {
|
||||||
|
ok = createSampler(device, true);
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuTexture::destroy(SDL_GPUDevice* device) {
|
||||||
|
if (device == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sampler_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUSampler(device, sampler_);
|
||||||
|
sampler_ = nullptr;
|
||||||
|
}
|
||||||
|
if (texture_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTexture(device, texture_);
|
||||||
|
texture_ = nullptr;
|
||||||
|
}
|
||||||
|
width_ = height_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Private helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
auto GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels, int w, int h, SDL_GPUTextureFormat format) -> bool {
|
||||||
|
// Create GPU texture
|
||||||
|
SDL_GPUTextureCreateInfo tex_info = {};
|
||||||
|
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||||
|
tex_info.format = format;
|
||||||
|
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||||
|
tex_info.width = static_cast<Uint32>(w);
|
||||||
|
tex_info.height = static_cast<Uint32>(h);
|
||||||
|
tex_info.layer_count_or_depth = 1;
|
||||||
|
tex_info.num_levels = 1;
|
||||||
|
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||||
|
|
||||||
|
texture_ = SDL_CreateGPUTexture(device, &tex_info);
|
||||||
|
if (texture_ == nullptr) {
|
||||||
|
SDL_Log("GpuTexture: SDL_CreateGPUTexture failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create transfer buffer and upload pixels
|
||||||
|
auto data_size = static_cast<Uint32>(w * h * 4); // RGBA = 4 bytes/pixel
|
||||||
|
|
||||||
|
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||||
|
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||||
|
tb_info.size = data_size;
|
||||||
|
|
||||||
|
SDL_GPUTransferBuffer* transfer = SDL_CreateGPUTransferBuffer(device, &tb_info);
|
||||||
|
if (transfer == nullptr) {
|
||||||
|
SDL_Log("GpuTexture: transfer buffer creation failed: %s", SDL_GetError());
|
||||||
|
SDL_ReleaseGPUTexture(device, texture_);
|
||||||
|
texture_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* mapped = SDL_MapGPUTransferBuffer(device, transfer, false);
|
||||||
|
if (mapped == nullptr) {
|
||||||
|
SDL_Log("GpuTexture: map failed: %s", SDL_GetError());
|
||||||
|
SDL_ReleaseGPUTransferBuffer(device, transfer);
|
||||||
|
SDL_ReleaseGPUTexture(device, texture_);
|
||||||
|
texture_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(mapped, pixels, data_size);
|
||||||
|
SDL_UnmapGPUTransferBuffer(device, transfer);
|
||||||
|
|
||||||
|
// Upload via command buffer
|
||||||
|
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device);
|
||||||
|
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd);
|
||||||
|
|
||||||
|
SDL_GPUTextureTransferInfo src = {};
|
||||||
|
src.transfer_buffer = transfer;
|
||||||
|
src.offset = 0;
|
||||||
|
src.pixels_per_row = static_cast<Uint32>(w);
|
||||||
|
src.rows_per_layer = static_cast<Uint32>(h);
|
||||||
|
|
||||||
|
SDL_GPUTextureRegion dst = {};
|
||||||
|
dst.texture = texture_;
|
||||||
|
dst.mip_level = 0;
|
||||||
|
dst.layer = 0;
|
||||||
|
dst.x = dst.y = dst.z = 0;
|
||||||
|
dst.w = static_cast<Uint32>(w);
|
||||||
|
dst.h = static_cast<Uint32>(h);
|
||||||
|
dst.d = 1;
|
||||||
|
|
||||||
|
SDL_UploadToGPUTexture(copy, &src, &dst, false);
|
||||||
|
SDL_EndGPUCopyPass(copy);
|
||||||
|
SDL_SubmitGPUCommandBuffer(cmd);
|
||||||
|
|
||||||
|
SDL_ReleaseGPUTransferBuffer(device, transfer);
|
||||||
|
width_ = w;
|
||||||
|
height_ = h;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuTexture::createSampler(SDL_GPUDevice* device, bool nearest) -> bool {
|
||||||
|
SDL_GPUSamplerCreateInfo info = {};
|
||||||
|
info.min_filter = nearest ? SDL_GPU_FILTER_NEAREST : SDL_GPU_FILTER_LINEAR;
|
||||||
|
info.mag_filter = nearest ? SDL_GPU_FILTER_NEAREST : SDL_GPU_FILTER_LINEAR;
|
||||||
|
info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
|
||||||
|
info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||||
|
info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||||
|
info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||||
|
|
||||||
|
sampler_ = SDL_CreateGPUSampler(device, &info);
|
||||||
|
if (sampler_ == nullptr) {
|
||||||
|
SDL_Log("GpuTexture: SDL_CreateGPUSampler failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
47
source/gpu/gpu_texture.hpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
#include <SDL3/SDL_surface.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GpuTexture — SDL_GPU texture + sampler wrapper
|
||||||
|
// Handles sprite textures, render targets, and the 1×1 white utility texture.
|
||||||
|
// ============================================================================
|
||||||
|
class GpuTexture {
|
||||||
|
public:
|
||||||
|
GpuTexture() = default;
|
||||||
|
~GpuTexture() = default;
|
||||||
|
|
||||||
|
// Load from resource path (pack or disk) using stb_image.
|
||||||
|
bool fromFile(SDL_GPUDevice* device, const std::string& file_path);
|
||||||
|
|
||||||
|
// Upload pixel data from an SDL_Surface to a new GPU texture + sampler.
|
||||||
|
// Uses nearest-neighbor filter for sprite pixel-perfect look.
|
||||||
|
bool fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest = true);
|
||||||
|
|
||||||
|
// Create an offscreen render target (COLOR_TARGET | SAMPLER usage).
|
||||||
|
bool createRenderTarget(SDL_GPUDevice* device, int w, int h, SDL_GPUTextureFormat format);
|
||||||
|
|
||||||
|
// Create a 1×1 opaque white texture (used for untextured geometry).
|
||||||
|
bool createWhite(SDL_GPUDevice* device);
|
||||||
|
|
||||||
|
// Release GPU resources.
|
||||||
|
void destroy(SDL_GPUDevice* device);
|
||||||
|
|
||||||
|
SDL_GPUTexture* texture() const { return texture_; }
|
||||||
|
SDL_GPUSampler* sampler() const { return sampler_; }
|
||||||
|
int width() const { return width_; }
|
||||||
|
int height() const { return height_; }
|
||||||
|
bool isValid() const { return texture_ != nullptr; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool uploadPixels(SDL_GPUDevice* device, const void* pixels, int w, int h, SDL_GPUTextureFormat format);
|
||||||
|
bool createSampler(SDL_GPUDevice* device, bool nearest);
|
||||||
|
|
||||||
|
SDL_GPUTexture* texture_ = nullptr;
|
||||||
|
SDL_GPUSampler* sampler_ = nullptr;
|
||||||
|
int width_ = 0;
|
||||||
|
int height_ = 0;
|
||||||
|
};
|
||||||
330
source/input/input_handler.cpp
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
#include "input_handler.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_keycode.h> // for SDL_Keycode
|
||||||
|
|
||||||
|
#include <string> // for std::string, std::to_string
|
||||||
|
|
||||||
|
#include "defines.hpp" // for KIOSK_NOTIFICATION_TEXT
|
||||||
|
#include "engine.hpp" // for Engine
|
||||||
|
#include "external/mouse.hpp" // for Mouse namespace
|
||||||
|
|
||||||
|
auto InputHandler::processEvents(Engine& engine) -> bool { // NOLINT(readability-function-cognitive-complexity)
|
||||||
|
SDL_Event event;
|
||||||
|
while (SDL_PollEvent(&event)) {
|
||||||
|
// Procesar eventos de ratón (auto-ocultar cursor)
|
||||||
|
Mouse::handleEvent(event);
|
||||||
|
|
||||||
|
// Salir del bucle si se detecta una petición de cierre
|
||||||
|
if (event.type == SDL_EVENT_QUIT) {
|
||||||
|
return true; // Solicitar salida
|
||||||
|
}
|
||||||
|
|
||||||
|
// Procesar eventos de teclado
|
||||||
|
if (event.type == SDL_EVENT_KEY_DOWN && static_cast<int>(event.key.repeat) == 0) {
|
||||||
|
switch (event.key.key) {
|
||||||
|
case SDLK_ESCAPE:
|
||||||
|
if (engine.isKioskMode()) {
|
||||||
|
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true; // Solicitar salida
|
||||||
|
|
||||||
|
case SDLK_SPACE:
|
||||||
|
engine.pushBallsAwayFromGravity();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_G:
|
||||||
|
engine.handleGravityToggle();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Controles de dirección de gravedad con teclas de cursor
|
||||||
|
case SDLK_UP:
|
||||||
|
engine.handleGravityDirectionChange(GravityDirection::UP, "Gravedad arriba");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_DOWN:
|
||||||
|
engine.handleGravityDirectionChange(GravityDirection::DOWN, "Gravedad abajo");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_LEFT:
|
||||||
|
engine.handleGravityDirectionChange(GravityDirection::LEFT, "Gravedad izquierda");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_RIGHT:
|
||||||
|
engine.handleGravityDirectionChange(GravityDirection::RIGHT, "Gravedad derecha");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_V:
|
||||||
|
engine.toggleVSync();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_H:
|
||||||
|
engine.toggleHelp(); // Toggle ayuda de teclas
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Toggle Física ↔ Última Figura (antes era C)
|
||||||
|
case SDLK_F:
|
||||||
|
engine.toggleShapeMode();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Selección directa de figuras 3D
|
||||||
|
case SDLK_Q:
|
||||||
|
engine.activateShape(ShapeType::SPHERE, "Esfera");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_W:
|
||||||
|
engine.activateShape(ShapeType::LISSAJOUS, "Lissajous");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_E:
|
||||||
|
engine.activateShape(ShapeType::HELIX, "Hélice");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_R:
|
||||||
|
engine.activateShape(ShapeType::TORUS, "Toroide");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_T:
|
||||||
|
engine.activateShape(ShapeType::CUBE, "Cubo");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_Y:
|
||||||
|
engine.activateShape(ShapeType::CYLINDER, "Cilindro");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_U:
|
||||||
|
engine.activateShape(ShapeType::ICOSAHEDRON, "Icosaedro");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_I:
|
||||||
|
engine.activateShape(ShapeType::ATOM, "Átomo");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_O:
|
||||||
|
// engine.activateShape(ShapeType::PNG_SHAPE, "Forma PNG");
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Toggle Modo Boids (comportamiento de enjambre)
|
||||||
|
case SDLK_B:
|
||||||
|
engine.toggleBoidsMode();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Ciclar temas de color (movido de B a C)
|
||||||
|
case SDLK_C: {
|
||||||
|
// Detectar si Shift está presionado
|
||||||
|
SDL_Keymod modstate = SDL_GetModState();
|
||||||
|
if ((modstate & SDL_KMOD_SHIFT) != 0u) {
|
||||||
|
// Shift+C: Ciclar hacia atrás (tema anterior)
|
||||||
|
engine.cycleTheme(false);
|
||||||
|
} else {
|
||||||
|
// C solo: Ciclar hacia adelante (tema siguiente)
|
||||||
|
engine.cycleTheme(true);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// Temas de colores con teclado numérico (con transición suave)
|
||||||
|
case SDLK_KP_1:
|
||||||
|
engine.switchThemeByNumpad(1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_KP_2:
|
||||||
|
engine.switchThemeByNumpad(2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_KP_3:
|
||||||
|
engine.switchThemeByNumpad(3);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_KP_4:
|
||||||
|
engine.switchThemeByNumpad(4);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_KP_5:
|
||||||
|
engine.switchThemeByNumpad(5);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_KP_6:
|
||||||
|
engine.switchThemeByNumpad(6);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_KP_7:
|
||||||
|
engine.switchThemeByNumpad(7);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_KP_8:
|
||||||
|
engine.switchThemeByNumpad(8);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_KP_9:
|
||||||
|
engine.switchThemeByNumpad(9);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_KP_0:
|
||||||
|
engine.switchThemeByNumpad(0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Toggle de página de temas (Numpad Enter)
|
||||||
|
case SDLK_KP_ENTER:
|
||||||
|
engine.toggleThemePage();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Cambio de sprite/textura dinámico
|
||||||
|
case SDLK_N:
|
||||||
|
engine.switchTexture();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Control de escala de figura (solo en modo SHAPE)
|
||||||
|
case SDLK_KP_PLUS:
|
||||||
|
engine.handleShapeScaleChange(true); // Aumentar
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_KP_MINUS:
|
||||||
|
engine.handleShapeScaleChange(false); // Disminuir
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_KP_MULTIPLY:
|
||||||
|
engine.resetShapeScale();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_KP_DIVIDE:
|
||||||
|
engine.toggleDepthZoom();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Cambio de número de pelotas (escenarios 1-8)
|
||||||
|
case SDLK_1:
|
||||||
|
engine.changeScenario(0, "10 pelotas");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_2:
|
||||||
|
engine.changeScenario(1, "50 pelotas");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_3:
|
||||||
|
engine.changeScenario(2, "100 pelotas");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_4:
|
||||||
|
engine.changeScenario(3, "500 pelotas");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_5:
|
||||||
|
engine.changeScenario(4, "1.000 pelotas");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_6:
|
||||||
|
engine.changeScenario(5, "5.000 pelotas");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_7:
|
||||||
|
engine.changeScenario(6, "10.000 pelotas");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_8:
|
||||||
|
engine.changeScenario(7, "50.000 pelotas");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_9:
|
||||||
|
if (engine.isCustomScenarioEnabled()) {
|
||||||
|
std::string custom_notif = std::to_string(engine.getCustomScenarioBalls()) + " pelotas";
|
||||||
|
engine.changeScenario(CUSTOM_SCENARIO_IDX, custom_notif.c_str());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Controles de zoom dinámico (solo si no estamos en fullscreen)
|
||||||
|
case SDLK_F1:
|
||||||
|
if (engine.isKioskMode()) {
|
||||||
|
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
|
||||||
|
} else {
|
||||||
|
engine.handleZoomOut();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_F2:
|
||||||
|
if (engine.isKioskMode()) {
|
||||||
|
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
|
||||||
|
} else {
|
||||||
|
engine.handleZoomIn();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Control de pantalla completa
|
||||||
|
case SDLK_F3:
|
||||||
|
if (engine.isKioskMode()) {
|
||||||
|
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
|
||||||
|
} else {
|
||||||
|
engine.toggleFullscreen();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Modo real fullscreen (cambia resolución interna)
|
||||||
|
case SDLK_F4:
|
||||||
|
if (engine.isKioskMode()) {
|
||||||
|
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
|
||||||
|
} else {
|
||||||
|
engine.toggleRealFullscreen();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Toggle PostFX activo/inactivo
|
||||||
|
case SDLK_F5:
|
||||||
|
engine.handlePostFXToggle();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Toggle escalado entero/estirado (solo en fullscreen F3)
|
||||||
|
case SDLK_F6:
|
||||||
|
engine.toggleIntegerScaling();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Redimensionar campo de juego (tamaño lógico + físico)
|
||||||
|
case SDLK_F7:
|
||||||
|
if (engine.isKioskMode()) {
|
||||||
|
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
|
||||||
|
} else {
|
||||||
|
engine.fieldSizeDown();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_F8:
|
||||||
|
if (engine.isKioskMode()) {
|
||||||
|
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
|
||||||
|
} else {
|
||||||
|
engine.fieldSizeUp();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Toggle Modo DEMO COMPLETO (auto-play) o Pausar tema dinámico (Shift+D)
|
||||||
|
case SDLK_D:
|
||||||
|
// Shift+D = Pausar tema dinámico
|
||||||
|
if ((event.key.mod & SDL_KMOD_SHIFT) != 0u) {
|
||||||
|
engine.pauseDynamicTheme();
|
||||||
|
} else {
|
||||||
|
// D sin Shift = Toggle DEMO ↔ SANDBOX
|
||||||
|
engine.toggleDemoMode();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Toggle Modo DEMO LITE (solo física/figuras)
|
||||||
|
case SDLK_L:
|
||||||
|
engine.toggleDemoLiteMode();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Toggle Modo LOGO (easter egg - marca de agua)
|
||||||
|
case SDLK_K:
|
||||||
|
engine.toggleLogoMode();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Ciclar presets PostFX (vinyeta/scanlines/cromàtica/complet/desactivat)
|
||||||
|
case SDLK_X:
|
||||||
|
engine.handlePostFXCycle();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Toggle Debug Display (movido de H a F12)
|
||||||
|
case SDLK_F12:
|
||||||
|
engine.toggleDebug();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // No se solicitó salida
|
||||||
|
}
|
||||||
32
source/input/input_handler.hpp
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_events.h> // for SDL_Event
|
||||||
|
|
||||||
|
// Forward declaration para evitar dependencia circular
|
||||||
|
class Engine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class InputHandler
|
||||||
|
* @brief Procesa eventos de entrada (teclado, ratón, ventana) y los traduce a acciones del Engine
|
||||||
|
*
|
||||||
|
* Responsabilidad única: Manejo de input SDL y traducción a comandos de alto nivel
|
||||||
|
*
|
||||||
|
* Características:
|
||||||
|
* - Procesa todos los eventos SDL (teclado, ratón, quit)
|
||||||
|
* - Traduce inputs a llamadas de métodos del Engine
|
||||||
|
* - Mantiene el Engine desacoplado de la lógica de input SDL
|
||||||
|
* - Soporta todos los controles del proyecto (gravedad, figuras, temas, zoom, fullscreen)
|
||||||
|
*/
|
||||||
|
class InputHandler {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Procesa todos los eventos SDL pendientes
|
||||||
|
* @param engine Referencia al engine para ejecutar acciones
|
||||||
|
* @return true si se debe salir de la aplicación (ESC o cerrar ventana), false en caso contrario
|
||||||
|
*/
|
||||||
|
static bool processEvents(Engine& engine);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Sin estado interno por ahora - el InputHandler es stateless
|
||||||
|
// Todos los estados se delegan al Engine
|
||||||
|
};
|
||||||
223
source/main.cpp
@@ -1,15 +1,230 @@
|
|||||||
|
#include <cstring>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include "engine.h"
|
#include "defines.hpp"
|
||||||
|
#include "engine.hpp"
|
||||||
|
#include "resource_manager.hpp"
|
||||||
|
|
||||||
|
// getExecutableDirectory() ya está definido en defines.h como inline
|
||||||
|
|
||||||
|
void printHelp() {
|
||||||
|
std::cout << "ViBe3 Physics - Simulador de físicas avanzadas\n";
|
||||||
|
std::cout << "\nUso: vibe3_physics [opciones]\n\n";
|
||||||
|
std::cout << "Opciones:\n";
|
||||||
|
std::cout << " -w, --width <px> Ancho de resolución (default: " << DEFAULT_SCREEN_WIDTH << ")\n";
|
||||||
|
std::cout << " -h, --height <px> Alto de resolución (default: " << DEFAULT_SCREEN_HEIGHT << ")\n";
|
||||||
|
std::cout << " -z, --zoom <n> Zoom de ventana (default: " << DEFAULT_WINDOW_ZOOM << ")\n";
|
||||||
|
std::cout << " -f, --fullscreen Modo pantalla completa (F3 - letterbox)\n";
|
||||||
|
std::cout << " -F, --real-fullscreen Modo pantalla completa real (F4 - nativo)\n";
|
||||||
|
std::cout << " -k, --kiosk Modo kiosko (F4 fijo, sin ESC, sin zoom)\n";
|
||||||
|
std::cout << " -m, --mode <mode> Modo inicial: sandbox, demo, demo-lite, logo (default: sandbox)\n";
|
||||||
|
std::cout << " --custom-balls <n> Activa escenario custom (tecla 9) con N pelotas\n";
|
||||||
|
std::cout << " --skip-benchmark Salta el benchmark y usa el máximo de bolas (50000)\n";
|
||||||
|
std::cout << " --max-balls <n> Limita el máximo de bolas en modos DEMO/DEMO_LITE\n";
|
||||||
|
std::cout << " --postfx [efecto] Arrancar con PostFX activo (default: complet): vinyeta, scanlines, cromatica, complet\n";
|
||||||
|
std::cout << " --vignette <float> Sobreescribir vignette_strength (activa PostFX si no hay --postfx)\n";
|
||||||
|
std::cout << " --chroma <float> Sobreescribir chroma_strength (activa PostFX si no hay --postfx)\n";
|
||||||
|
std::cout << " --help Mostrar esta ayuda\n\n";
|
||||||
|
std::cout << "Ejemplos:\n";
|
||||||
|
std::cout << " vibe3_physics # " << DEFAULT_SCREEN_WIDTH << "x" << DEFAULT_SCREEN_HEIGHT << " zoom " << DEFAULT_WINDOW_ZOOM << " (default)\n";
|
||||||
|
std::cout << " vibe3_physics -w 1920 -h 1080 # 1920x1080 zoom 1 (auto)\n";
|
||||||
|
std::cout << " vibe3_physics -w 640 -h 480 -z 2 # 640x480 zoom 2 (ventana 1280x960)\n";
|
||||||
|
std::cout << " vibe3_physics -f # Fullscreen letterbox (F3)\n";
|
||||||
|
std::cout << " vibe3_physics -F # Fullscreen real (F4 - resolución nativa)\n";
|
||||||
|
std::cout << " vibe3_physics -k # Modo kiosko (pantalla completa real, bloqueado)\n";
|
||||||
|
std::cout << " vibe3_physics --mode demo # Arrancar en modo DEMO (auto-play)\n";
|
||||||
|
std::cout << " vibe3_physics -m demo-lite # Arrancar en modo DEMO_LITE (solo física)\n";
|
||||||
|
std::cout << " vibe3_physics -F --mode logo # Fullscreen + modo LOGO (easter egg)\n\n";
|
||||||
|
std::cout << "Nota: Si resolución > pantalla, se usa default. Zoom se ajusta automáticamente.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto main(int argc, char* argv[]) -> int { // NOLINT(readability-function-cognitive-complexity)
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
int zoom = 0;
|
||||||
|
int custom_balls = 0;
|
||||||
|
bool fullscreen = false;
|
||||||
|
bool real_fullscreen = false;
|
||||||
|
bool kiosk_mode = false;
|
||||||
|
bool skip_benchmark = false;
|
||||||
|
int max_balls_override = 0;
|
||||||
|
int initial_postfx = -1;
|
||||||
|
float override_vignette = -1.f;
|
||||||
|
float override_chroma = -1.f;
|
||||||
|
AppMode initial_mode = AppMode::SANDBOX; // Modo inicial (default: SANDBOX)
|
||||||
|
|
||||||
|
// Parsear argumentos
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
if (strcmp(argv[i], "--help") == 0) {
|
||||||
|
printHelp();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (strcmp(argv[i], "-w") == 0 || strcmp(argv[i], "--width") == 0) {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
width = atoi(argv[++i]);
|
||||||
|
if (width < 320) {
|
||||||
|
std::cerr << "Error: Ancho mínimo es 320\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: -w/--width requiere un valor\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--height") == 0) {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
height = atoi(argv[++i]);
|
||||||
|
if (height < 240) {
|
||||||
|
std::cerr << "Error: Alto mínimo es 240\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: -h/--height requiere un valor\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else if (strcmp(argv[i], "-z") == 0 || strcmp(argv[i], "--zoom") == 0) {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
zoom = atoi(argv[++i]);
|
||||||
|
if (zoom < 1) {
|
||||||
|
std::cerr << "Error: Zoom mínimo es 1\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: -z/--zoom requiere un valor\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else if (strcmp(argv[i], "-f") == 0 || strcmp(argv[i], "--fullscreen") == 0) {
|
||||||
|
fullscreen = true;
|
||||||
|
} else if (strcmp(argv[i], "-F") == 0 || strcmp(argv[i], "--real-fullscreen") == 0) {
|
||||||
|
real_fullscreen = true;
|
||||||
|
} else if (strcmp(argv[i], "-k") == 0 || strcmp(argv[i], "--kiosk") == 0) {
|
||||||
|
kiosk_mode = true;
|
||||||
|
} else if (strcmp(argv[i], "-m") == 0 || strcmp(argv[i], "--mode") == 0) {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
std::string mode_str = argv[++i];
|
||||||
|
if (mode_str == "sandbox") {
|
||||||
|
initial_mode = AppMode::SANDBOX;
|
||||||
|
} else if (mode_str == "demo") {
|
||||||
|
initial_mode = AppMode::DEMO;
|
||||||
|
} else if (mode_str == "demo-lite") {
|
||||||
|
initial_mode = AppMode::DEMO_LITE;
|
||||||
|
} else if (mode_str == "logo") {
|
||||||
|
initial_mode = AppMode::LOGO;
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: Modo '" << mode_str << "' no válido. Usa: sandbox, demo, demo-lite, logo\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: -m/--mode requiere un valor\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else if (strcmp(argv[i], "--custom-balls") == 0) {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
int n = atoi(argv[++i]);
|
||||||
|
if (n < 1) {
|
||||||
|
std::cerr << "Error: --custom-balls requiere un valor >= 1\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
custom_balls = n;
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --custom-balls requiere un valor\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else if (strcmp(argv[i], "--skip-benchmark") == 0) {
|
||||||
|
skip_benchmark = true;
|
||||||
|
} else if (strcmp(argv[i], "--postfx") == 0) {
|
||||||
|
// Si no hay valor o el siguiente arg es otra opción, defaultear a complet
|
||||||
|
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
||||||
|
std::string fx = argv[++i];
|
||||||
|
if (fx == "vinyeta") {
|
||||||
|
initial_postfx = 0;
|
||||||
|
} else if (fx == "scanlines") {
|
||||||
|
initial_postfx = 1;
|
||||||
|
} else if (fx == "cromatica") {
|
||||||
|
initial_postfx = 2;
|
||||||
|
} else if (fx == "complet") {
|
||||||
|
initial_postfx = 3;
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --postfx '" << fx << "' no válido. Usa: vinyeta, scanlines, cromatica, complet\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
initial_postfx = 3; // default: complet
|
||||||
|
}
|
||||||
|
} else if (strcmp(argv[i], "--vignette") == 0) {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
override_vignette = (float)atof(argv[++i]);
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --vignette requiere un valor\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else if (strcmp(argv[i], "--chroma") == 0) {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
override_chroma = (float)atof(argv[++i]);
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --chroma requiere un valor\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else if (strcmp(argv[i], "--max-balls") == 0) {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
int n = atoi(argv[++i]);
|
||||||
|
if (n < 1) {
|
||||||
|
std::cerr << "Error: --max-balls requiere un valor >= 1\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
max_balls_override = n;
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --max-balls requiere un valor\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: Opción desconocida '" << argv[i] << "'\n";
|
||||||
|
printHelp();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializar sistema de recursos empaquetados (intentar cargar resources.pack)
|
||||||
|
std::string resources_dir = getResourcesDirectory();
|
||||||
|
std::string pack_path = resources_dir + "/resources.pack";
|
||||||
|
ResourceManager::init(pack_path);
|
||||||
|
|
||||||
int main() {
|
|
||||||
Engine engine;
|
Engine engine;
|
||||||
|
|
||||||
if (!engine.initialize()) {
|
if (custom_balls > 0) {
|
||||||
std::cout << "¡Error al inicializar el engine!" << std::endl;
|
engine.setCustomScenario(custom_balls); // pre-init: asigna campos antes del benchmark
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max_balls_override > 0) {
|
||||||
|
engine.setMaxBallsOverride(max_balls_override);
|
||||||
|
} else if (skip_benchmark) {
|
||||||
|
engine.setSkipBenchmark();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initial_postfx >= 0) {
|
||||||
|
engine.setInitialPostFX(initial_postfx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (override_vignette >= 0.f || override_chroma >= 0.f) {
|
||||||
|
if (initial_postfx < 0) {
|
||||||
|
engine.setInitialPostFX(0);
|
||||||
|
}
|
||||||
|
engine.setPostFXParamOverrides(override_vignette, override_chroma);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!engine.initialize(width, height, zoom, fullscreen, initial_mode)) {
|
||||||
|
std::cout << "¡Error al inicializar el engine!" << '\n';
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Si se especificó real fullscreen (F4) o modo kiosko, activar después de inicializar
|
||||||
|
if (real_fullscreen || kiosk_mode) {
|
||||||
|
engine.toggleRealFullscreen();
|
||||||
|
}
|
||||||
|
if (kiosk_mode) {
|
||||||
|
engine.setKioskMode(true);
|
||||||
|
}
|
||||||
|
|
||||||
engine.run();
|
engine.run();
|
||||||
engine.shutdown();
|
engine.shutdown();
|
||||||
|
|
||||||
|
|||||||
100
source/resource_manager.cpp
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#include "resource_manager.hpp"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "resource_pack.hpp"
|
||||||
|
|
||||||
|
// Inicializar estáticos
|
||||||
|
ResourcePack* ResourceManager::resourcePack_ = nullptr;
|
||||||
|
std::map<std::string, std::vector<unsigned char>> ResourceManager::cache_;
|
||||||
|
|
||||||
|
auto ResourceManager::init(const std::string& pack_file_path) -> bool {
|
||||||
|
// Si ya estaba inicializado, liberar primero
|
||||||
|
if (resourcePack_ != nullptr) {
|
||||||
|
delete resourcePack_;
|
||||||
|
resourcePack_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intentar cargar el pack
|
||||||
|
resourcePack_ = new ResourcePack();
|
||||||
|
if (!resourcePack_->loadPack(pack_file_path)) {
|
||||||
|
// Si falla, borrar instancia (usará fallback a disco)
|
||||||
|
delete resourcePack_;
|
||||||
|
resourcePack_ = nullptr;
|
||||||
|
std::cout << "resources.pack no encontrado - usando carpeta data/" << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "resources.pack cargado (" << resourcePack_->getResourceCount() << " recursos)" << '\n';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResourceManager::shutdown() {
|
||||||
|
cache_.clear();
|
||||||
|
if (resourcePack_ != nullptr) {
|
||||||
|
delete resourcePack_;
|
||||||
|
resourcePack_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourceManager::loadResource(const std::string& resource_path, unsigned char*& data, size_t& size) -> bool {
|
||||||
|
data = nullptr;
|
||||||
|
size = 0;
|
||||||
|
|
||||||
|
// 1. Consultar caché en RAM (sin I/O)
|
||||||
|
auto it = cache_.find(resource_path);
|
||||||
|
if (it != cache_.end()) {
|
||||||
|
size = it->second.size();
|
||||||
|
data = new unsigned char[size];
|
||||||
|
std::memcpy(data, it->second.data(), size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Intentar cargar desde pack (si está disponible)
|
||||||
|
if (resourcePack_ != nullptr) {
|
||||||
|
ResourcePack::ResourceData pack_data = resourcePack_->loadResource(resource_path);
|
||||||
|
if (pack_data.data != nullptr) {
|
||||||
|
cache_[resource_path] = std::vector<unsigned char>(pack_data.data, pack_data.data + pack_data.size);
|
||||||
|
data = pack_data.data;
|
||||||
|
size = pack_data.size;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback: cargar desde disco
|
||||||
|
std::ifstream file(resource_path, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file) {
|
||||||
|
std::string data_path = "data/" + resource_path;
|
||||||
|
file.open(data_path, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file) { return false; }
|
||||||
|
}
|
||||||
|
size = static_cast<size_t>(file.tellg());
|
||||||
|
file.seekg(0, std::ios::beg);
|
||||||
|
data = new unsigned char[size];
|
||||||
|
file.read(reinterpret_cast<char*>(data), size);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
// Guardar en caché
|
||||||
|
cache_[resource_path] = std::vector<unsigned char>(data, data + size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourceManager::isPackLoaded() -> bool {
|
||||||
|
return resourcePack_ != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourceManager::getResourceList() -> std::vector<std::string> {
|
||||||
|
if (resourcePack_ != nullptr) {
|
||||||
|
return resourcePack_->getResourceList();
|
||||||
|
}
|
||||||
|
return {}; // Vacío si no hay pack
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourceManager::getResourceCount() -> size_t {
|
||||||
|
if (resourcePack_ != nullptr) {
|
||||||
|
return resourcePack_->getResourceCount();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
86
source/resource_manager.hpp
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class ResourcePack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ResourceManager - Gestor centralizado de recursos empaquetados
|
||||||
|
*
|
||||||
|
* Singleton que administra el sistema de recursos empaquetados (resources.pack)
|
||||||
|
* y proporciona fallback automático a disco cuando el pack no está disponible.
|
||||||
|
*
|
||||||
|
* Uso:
|
||||||
|
* // En main.cpp, antes de inicializar cualquier sistema:
|
||||||
|
* ResourceManager::init("resources.pack");
|
||||||
|
*
|
||||||
|
* // Desde cualquier clase que necesite recursos:
|
||||||
|
* unsigned char* data = nullptr;
|
||||||
|
* size_t size = 0;
|
||||||
|
* if (ResourceManager::loadResource("textures/ball.png", data, size)) {
|
||||||
|
* // Usar datos...
|
||||||
|
* delete[] data; // Liberar cuando termine
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
class ResourceManager {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Inicializa el sistema de recursos empaquetados
|
||||||
|
* Debe llamarse una única vez al inicio del programa
|
||||||
|
*
|
||||||
|
* @param packFilePath Ruta al archivo .pack (ej: "resources.pack")
|
||||||
|
* @return true si el pack se cargó correctamente, false si no existe (fallback a disco)
|
||||||
|
*/
|
||||||
|
static bool init(const std::string& pack_file_path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Libera el sistema de recursos
|
||||||
|
* Opcional - se llama automáticamente al cerrar el programa
|
||||||
|
*/
|
||||||
|
static void shutdown();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carga un recurso desde el pack (o disco si no existe pack)
|
||||||
|
*
|
||||||
|
* @param resourcePath Ruta relativa del recurso (ej: "textures/ball.png")
|
||||||
|
* @param data [out] Puntero donde se almacenará el buffer (debe liberar con delete[])
|
||||||
|
* @param size [out] Tamaño del buffer en bytes
|
||||||
|
* @return true si se cargó correctamente, false si falla
|
||||||
|
*/
|
||||||
|
static bool loadResource(const std::string& resource_path, unsigned char*& data, size_t& size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si el pack está cargado
|
||||||
|
* @return true si hay un pack cargado, false si se usa disco
|
||||||
|
*/
|
||||||
|
static bool isPackLoaded();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene la lista de recursos disponibles en el pack
|
||||||
|
* @return Vector con las rutas de todos los recursos, vacío si no hay pack
|
||||||
|
*/
|
||||||
|
static std::vector<std::string> getResourceList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene el número de recursos en el pack
|
||||||
|
* @return Número de recursos, 0 si no hay pack
|
||||||
|
*/
|
||||||
|
static size_t getResourceCount();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Constructor privado (singleton)
|
||||||
|
ResourceManager() = default;
|
||||||
|
~ResourceManager() = default;
|
||||||
|
|
||||||
|
// Deshabilitar copia y asignación
|
||||||
|
ResourceManager(const ResourceManager&) = delete;
|
||||||
|
ResourceManager& operator=(const ResourceManager&) = delete;
|
||||||
|
|
||||||
|
// Instancia del pack (nullptr si no está cargado)
|
||||||
|
static ResourcePack* resourcePack_;
|
||||||
|
|
||||||
|
// Caché en RAM para evitar I/O repetido en el bucle principal
|
||||||
|
static std::map<std::string, std::vector<unsigned char>> cache_;
|
||||||
|
};
|
||||||
260
source/resource_pack.cpp
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
#include "resource_pack.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
ResourcePack::ResourcePack()
|
||||||
|
: isLoaded_(false) {}
|
||||||
|
|
||||||
|
ResourcePack::~ResourcePack() {
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// EMPAQUETADO (herramienta pack_resources)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
auto ResourcePack::addDirectory(const std::string& dir_path, const std::string& prefix) -> bool {
|
||||||
|
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
|
||||||
|
std::cerr << "Error: Directorio no existe: " << dir_path << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
|
||||||
|
if (entry.is_regular_file()) {
|
||||||
|
// Construir ruta relativa desde data/ (ej: "data/ball.png" → "ball.png")
|
||||||
|
std::string relative_path = fs::relative(entry.path(), dir_path).string();
|
||||||
|
std::string full_path = prefix.empty() ? relative_path : prefix + "/" + relative_path;
|
||||||
|
full_path = normalizePath(full_path);
|
||||||
|
|
||||||
|
// Leer archivo completo
|
||||||
|
std::ifstream file(entry.path(), std::ios::binary);
|
||||||
|
if (!file) {
|
||||||
|
std::cerr << "Error: No se pudo abrir: " << entry.path() << '\n';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.seekg(0, std::ios::end);
|
||||||
|
size_t file_size = file.tellg();
|
||||||
|
file.seekg(0, std::ios::beg);
|
||||||
|
|
||||||
|
std::vector<unsigned char> buffer(file_size);
|
||||||
|
file.read(reinterpret_cast<char*>(buffer.data()), file_size);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
// Crear entrada de recurso
|
||||||
|
ResourceEntry resource;
|
||||||
|
resource.path = full_path;
|
||||||
|
resource.offset = 0; // Se calculará al guardar
|
||||||
|
resource.size = static_cast<uint32_t>(file_size);
|
||||||
|
resource.checksum = calculateChecksum(buffer.data(), file_size);
|
||||||
|
|
||||||
|
resources_[full_path] = resource;
|
||||||
|
|
||||||
|
std::cout << " Añadido: " << full_path << " (" << file_size << " bytes)" << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return !resources_.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourcePack::savePack(const std::string& pack_file_path) -> bool {
|
||||||
|
std::ofstream pack_file(pack_file_path, std::ios::binary);
|
||||||
|
if (!pack_file) {
|
||||||
|
std::cerr << "Error: No se pudo crear pack: " << pack_file_path << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Escribir header
|
||||||
|
PackHeader header;
|
||||||
|
std::memcpy(header.magic, "VBE3", 4);
|
||||||
|
header.version = 1;
|
||||||
|
header.fileCount = static_cast<uint32_t>(resources_.size());
|
||||||
|
pack_file.write(reinterpret_cast<const char*>(&header), sizeof(PackHeader));
|
||||||
|
|
||||||
|
// 2. Calcular offsets (después del header + índice)
|
||||||
|
uint32_t current_offset = sizeof(PackHeader);
|
||||||
|
|
||||||
|
// Calcular tamaño del índice (cada entrada: uint32_t pathLen + path + 3*uint32_t)
|
||||||
|
for (const auto& [path, entry] : resources_) {
|
||||||
|
current_offset += sizeof(uint32_t); // pathLen
|
||||||
|
current_offset += static_cast<uint32_t>(path.size()); // path
|
||||||
|
current_offset += sizeof(uint32_t) * 3; // offset, size, checksum
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Escribir índice
|
||||||
|
for (auto& [path, entry] : resources_) {
|
||||||
|
entry.offset = current_offset;
|
||||||
|
|
||||||
|
auto path_len = static_cast<uint32_t>(path.size());
|
||||||
|
pack_file.write(reinterpret_cast<const char*>(&path_len), sizeof(uint32_t));
|
||||||
|
pack_file.write(path.c_str(), path_len);
|
||||||
|
pack_file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(uint32_t));
|
||||||
|
pack_file.write(reinterpret_cast<const char*>(&entry.size), sizeof(uint32_t));
|
||||||
|
pack_file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(uint32_t));
|
||||||
|
|
||||||
|
current_offset += entry.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Escribir datos de archivos (sin encriptar en pack, se encripta al cargar)
|
||||||
|
for (const auto& [path, entry] : resources_) {
|
||||||
|
// Encontrar archivo original en disco
|
||||||
|
fs::path original_path = fs::current_path() / "data" / path;
|
||||||
|
std::ifstream file(original_path, std::ios::binary);
|
||||||
|
if (!file) {
|
||||||
|
std::cerr << "Error: No se pudo re-leer: " << original_path << '\n';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> buffer(entry.size);
|
||||||
|
file.read(reinterpret_cast<char*>(buffer.data()), entry.size);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
pack_file.write(reinterpret_cast<const char*>(buffer.data()), entry.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
pack_file.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DESEMPAQUETADO (juego)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
auto ResourcePack::loadPack(const std::string& pack_file_path) -> bool {
|
||||||
|
clear();
|
||||||
|
|
||||||
|
packFile_.open(pack_file_path, std::ios::binary);
|
||||||
|
if (!packFile_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Leer header
|
||||||
|
PackHeader header;
|
||||||
|
packFile_.read(reinterpret_cast<char*>(&header), sizeof(PackHeader));
|
||||||
|
|
||||||
|
if (std::memcmp(header.magic, "VBE3", 4) != 0) {
|
||||||
|
std::cerr << "Error: Pack inválido (magic incorrecto)" << '\n';
|
||||||
|
packFile_.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.version != 1) {
|
||||||
|
std::cerr << "Error: Versión de pack no soportada: " << header.version << '\n';
|
||||||
|
packFile_.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Leer índice
|
||||||
|
for (uint32_t i = 0; i < header.fileCount; i++) {
|
||||||
|
ResourceEntry entry;
|
||||||
|
|
||||||
|
uint32_t path_len;
|
||||||
|
packFile_.read(reinterpret_cast<char*>(&path_len), sizeof(uint32_t));
|
||||||
|
|
||||||
|
std::vector<char> path_buffer(path_len + 1, '\0');
|
||||||
|
packFile_.read(path_buffer.data(), path_len);
|
||||||
|
entry.path = std::string(path_buffer.data());
|
||||||
|
|
||||||
|
packFile_.read(reinterpret_cast<char*>(&entry.offset), sizeof(uint32_t));
|
||||||
|
packFile_.read(reinterpret_cast<char*>(&entry.size), sizeof(uint32_t));
|
||||||
|
packFile_.read(reinterpret_cast<char*>(&entry.checksum), sizeof(uint32_t));
|
||||||
|
|
||||||
|
resources_[entry.path] = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoaded_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourcePack::loadResource(const std::string& resource_path) -> ResourcePack::ResourceData {
|
||||||
|
ResourceData result = {.data = nullptr, .size = 0};
|
||||||
|
|
||||||
|
if (!isLoaded_) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string normalized_path = normalizePath(resource_path);
|
||||||
|
auto it = resources_.find(normalized_path);
|
||||||
|
if (it == resources_.end()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResourceEntry& entry = it->second;
|
||||||
|
|
||||||
|
// Leer datos desde el pack
|
||||||
|
packFile_.seekg(entry.offset);
|
||||||
|
result.data = new unsigned char[entry.size];
|
||||||
|
result.size = entry.size;
|
||||||
|
packFile_.read(reinterpret_cast<char*>(result.data), entry.size);
|
||||||
|
|
||||||
|
// Verificar checksum
|
||||||
|
uint32_t checksum = calculateChecksum(result.data, entry.size);
|
||||||
|
if (checksum != entry.checksum) {
|
||||||
|
std::cerr << "Warning: Checksum incorrecto para: " << resource_path << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// UTILIDADES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
auto ResourcePack::getResourceList() const -> std::vector<std::string> {
|
||||||
|
std::vector<std::string> list;
|
||||||
|
list.reserve(resources_.size());
|
||||||
|
for (const auto& [path, entry] : resources_) {
|
||||||
|
list.push_back(path);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourcePack::getResourceCount() const -> size_t {
|
||||||
|
return resources_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResourcePack::clear() {
|
||||||
|
resources_.clear();
|
||||||
|
if (packFile_.is_open()) {
|
||||||
|
packFile_.close();
|
||||||
|
}
|
||||||
|
isLoaded_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// FUNCIONES AUXILIARES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
auto ResourcePack::calculateChecksum(const unsigned char* data, size_t size) -> uint32_t {
|
||||||
|
uint32_t checksum = 0;
|
||||||
|
for (size_t i = 0; i < size; i++) {
|
||||||
|
checksum ^= static_cast<uint32_t>(data[i]);
|
||||||
|
checksum = (checksum << 1) | (checksum >> 31); // Rotate left
|
||||||
|
}
|
||||||
|
return checksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourcePack::normalizePath(const std::string& path) -> std::string {
|
||||||
|
std::string normalized = path;
|
||||||
|
|
||||||
|
// Reemplazar \ por /
|
||||||
|
std::ranges::replace(normalized, '\\', '/');
|
||||||
|
|
||||||
|
// Buscar "data/" en cualquier parte del path y extraer lo que viene después
|
||||||
|
size_t data_pos = normalized.find("data/");
|
||||||
|
if (data_pos != std::string::npos) {
|
||||||
|
normalized = normalized.substr(data_pos + 5); // +5 para saltar "data/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar ./ del inicio si quedó
|
||||||
|
if (normalized.substr(0, 2) == "./") {
|
||||||
|
normalized = normalized.substr(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
63
source/resource_pack.hpp
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <fstream>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ResourcePack - Sistema de empaquetado de recursos para ViBe3 Physics
|
||||||
|
*
|
||||||
|
* Permite empaquetar todos los recursos (imágenes, etc.) en un archivo binario
|
||||||
|
* único y ofuscado. Proporciona fallback automático a carpeta data/ si no existe pack.
|
||||||
|
*/
|
||||||
|
class ResourcePack {
|
||||||
|
public:
|
||||||
|
ResourcePack();
|
||||||
|
~ResourcePack();
|
||||||
|
|
||||||
|
// Empaquetado (usado por herramienta pack_resources)
|
||||||
|
bool addDirectory(const std::string& dir_path, const std::string& prefix = "");
|
||||||
|
bool savePack(const std::string& pack_file_path);
|
||||||
|
|
||||||
|
// Desempaquetado (usado por el juego)
|
||||||
|
bool loadPack(const std::string& pack_file_path);
|
||||||
|
|
||||||
|
// Carga de recursos individuales
|
||||||
|
struct ResourceData {
|
||||||
|
unsigned char* data;
|
||||||
|
size_t size;
|
||||||
|
};
|
||||||
|
ResourceData loadResource(const std::string& resource_path);
|
||||||
|
|
||||||
|
// Utilidades
|
||||||
|
std::vector<std::string> getResourceList() const;
|
||||||
|
size_t getResourceCount() const;
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Header del pack (12 bytes)
|
||||||
|
struct PackHeader {
|
||||||
|
char magic[4]; // "VBE3"
|
||||||
|
uint32_t version; // Versión del formato (1)
|
||||||
|
uint32_t fileCount; // Número de archivos empaquetados
|
||||||
|
};
|
||||||
|
|
||||||
|
// Índice de un recurso (variable length)
|
||||||
|
struct ResourceEntry {
|
||||||
|
std::string path; // Ruta relativa del recurso
|
||||||
|
uint32_t offset; // Offset en el archivo pack
|
||||||
|
uint32_t size; // Tamaño en bytes
|
||||||
|
uint32_t checksum; // Checksum simple (XOR de bytes)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Datos internos
|
||||||
|
std::map<std::string, ResourceEntry> resources_;
|
||||||
|
std::ifstream packFile_;
|
||||||
|
bool isLoaded_;
|
||||||
|
|
||||||
|
// Funciones auxiliares
|
||||||
|
static uint32_t calculateChecksum(const unsigned char* data, size_t size);
|
||||||
|
static std::string normalizePath(const std::string& path);
|
||||||
|
};
|
||||||
267
source/scene/scene_manager.cpp
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
#include "scene_manager.hpp"
|
||||||
|
|
||||||
|
#include <cstdlib> // for rand
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "defines.hpp" // for BALL_COUNT_SCENARIOS, GRAVITY_MASS_MIN, etc
|
||||||
|
#include "external/texture.hpp" // for Texture
|
||||||
|
#include "theme_manager.hpp" // for ThemeManager
|
||||||
|
|
||||||
|
SceneManager::SceneManager(int screen_width, int screen_height)
|
||||||
|
: current_gravity_(GravityDirection::DOWN),
|
||||||
|
scenario_(0),
|
||||||
|
screen_width_(screen_width),
|
||||||
|
screen_height_(screen_height),
|
||||||
|
current_ball_size_(10),
|
||||||
|
texture_(nullptr),
|
||||||
|
theme_manager_(nullptr) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::initialize(int scenario, std::shared_ptr<Texture> texture, ThemeManager* theme_manager) {
|
||||||
|
scenario_ = scenario;
|
||||||
|
texture_ = std::move(texture);
|
||||||
|
theme_manager_ = theme_manager;
|
||||||
|
current_ball_size_ = texture_->getWidth();
|
||||||
|
|
||||||
|
// Crear bolas iniciales (siempre en modo PHYSICS al inicio)
|
||||||
|
changeScenario(scenario_, SimulationMode::PHYSICS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::update(float delta_time) {
|
||||||
|
// Actualizar física de todas las bolas
|
||||||
|
for (auto& ball : balls_) {
|
||||||
|
ball->update(delta_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::changeScenario(int scenario_id, SimulationMode mode) {
|
||||||
|
// Guardar escenario
|
||||||
|
scenario_ = scenario_id;
|
||||||
|
|
||||||
|
// Limpiar las bolas actuales
|
||||||
|
balls_.clear();
|
||||||
|
|
||||||
|
// Resetear gravedad al estado por defecto (DOWN) al cambiar escenario
|
||||||
|
changeGravityDirection(GravityDirection::DOWN);
|
||||||
|
|
||||||
|
// Crear las bolas según el escenario
|
||||||
|
int ball_count = (scenario_id == CUSTOM_SCENARIO_IDX)
|
||||||
|
? custom_ball_count_
|
||||||
|
: BALL_COUNT_SCENARIOS[scenario_id];
|
||||||
|
for (int i = 0; i < ball_count; ++i) {
|
||||||
|
float x;
|
||||||
|
float y;
|
||||||
|
float vx;
|
||||||
|
float vy;
|
||||||
|
|
||||||
|
// Inicialización según SimulationMode (RULES.md líneas 23-26)
|
||||||
|
switch (mode) {
|
||||||
|
case SimulationMode::PHYSICS: {
|
||||||
|
// PHYSICS: Parte superior, 75% distribución central en X
|
||||||
|
const int SIGN = ((rand() % 2) * 2) - 1;
|
||||||
|
const int MARGIN = static_cast<int>(screen_width_ * BALL_SPAWN_MARGIN);
|
||||||
|
const int SPAWN_ZONE_WIDTH = screen_width_ - (2 * MARGIN);
|
||||||
|
x = (rand() % SPAWN_ZONE_WIDTH) + MARGIN;
|
||||||
|
y = 0.0f; // Parte superior
|
||||||
|
vx = (((rand() % 20) + 10) * 0.1f) * SIGN;
|
||||||
|
vy = ((rand() % 60) - 30) * 0.1f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SimulationMode::SHAPE: {
|
||||||
|
// SHAPE: Centro de pantalla, sin velocidad inicial
|
||||||
|
x = screen_width_ / 2.0f;
|
||||||
|
y = screen_height_ / 2.0f; // Centro vertical
|
||||||
|
vx = 0.0f;
|
||||||
|
vy = 0.0f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SimulationMode::BOIDS: {
|
||||||
|
// BOIDS: Posiciones aleatorias, velocidades aleatorias
|
||||||
|
const int SIGN_X = ((rand() % 2) * 2) - 1;
|
||||||
|
const int SIGN_Y = ((rand() % 2) * 2) - 1;
|
||||||
|
x = static_cast<float>(rand() % screen_width_);
|
||||||
|
y = static_cast<float>(rand() % screen_height_); // Posición Y aleatoria
|
||||||
|
vx = (((rand() % 40) + 10) * 0.1f) * SIGN_X; // 1.0 - 5.0 px/frame
|
||||||
|
vy = (((rand() % 40) + 10) * 0.1f) * SIGN_Y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Fallback a PHYSICS por seguridad
|
||||||
|
const int SIGN = ((rand() % 2) * 2) - 1;
|
||||||
|
const int MARGIN = static_cast<int>(screen_width_ * BALL_SPAWN_MARGIN);
|
||||||
|
const int SPAWN_ZONE_WIDTH = screen_width_ - (2 * MARGIN);
|
||||||
|
x = (rand() % SPAWN_ZONE_WIDTH) + MARGIN;
|
||||||
|
y = 0.0f; // Parte superior
|
||||||
|
vx = (((rand() % 20) + 10) * 0.1f) * SIGN;
|
||||||
|
vy = ((rand() % 60) - 30) * 0.1f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seleccionar color de la paleta del tema actual (delegado a ThemeManager)
|
||||||
|
int random_index = rand();
|
||||||
|
Color color = theme_manager_->getInitialBallColor(random_index);
|
||||||
|
|
||||||
|
// Generar factor de masa aleatorio (0.7 = ligera, 1.3 = pesada)
|
||||||
|
float mass_factor = GRAVITY_MASS_MIN + ((rand() % 1000) / 1000.0f * (GRAVITY_MASS_MAX - GRAVITY_MASS_MIN));
|
||||||
|
|
||||||
|
balls_.emplace_back(std::make_unique<Ball>(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
vx,
|
||||||
|
vy,
|
||||||
|
color,
|
||||||
|
texture_,
|
||||||
|
screen_width_,
|
||||||
|
screen_height_,
|
||||||
|
current_ball_size_,
|
||||||
|
current_gravity_,
|
||||||
|
mass_factor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::updateBallTexture(std::shared_ptr<Texture> new_texture, int new_ball_size) {
|
||||||
|
if (balls_.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardar tamaño antiguo
|
||||||
|
int old_size = current_ball_size_;
|
||||||
|
|
||||||
|
// Actualizar textura y tamaño
|
||||||
|
texture_ = std::move(new_texture);
|
||||||
|
current_ball_size_ = new_ball_size;
|
||||||
|
|
||||||
|
// Actualizar texturas de todas las pelotas
|
||||||
|
for (auto& ball : balls_) {
|
||||||
|
ball->setTexture(texture_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajustar posiciones según el cambio de tamaño
|
||||||
|
updateBallSizes(old_size, new_ball_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::pushBallsAwayFromGravity() {
|
||||||
|
for (auto& ball : balls_) {
|
||||||
|
const int SIGNO = ((rand() % 2) * 2) - 1;
|
||||||
|
const float LATERAL = (((rand() % 20) + 10) * 0.1f) * SIGNO;
|
||||||
|
const float MAIN = ((rand() % 40) * 0.1f) + 5;
|
||||||
|
|
||||||
|
float vx = 0;
|
||||||
|
float vy = 0;
|
||||||
|
switch (current_gravity_) {
|
||||||
|
case GravityDirection::DOWN: // Impulsar ARRIBA
|
||||||
|
vx = LATERAL;
|
||||||
|
vy = -MAIN;
|
||||||
|
break;
|
||||||
|
case GravityDirection::UP: // Impulsar ABAJO
|
||||||
|
vx = LATERAL;
|
||||||
|
vy = MAIN;
|
||||||
|
break;
|
||||||
|
case GravityDirection::LEFT: // Impulsar DERECHA
|
||||||
|
vx = MAIN;
|
||||||
|
vy = LATERAL;
|
||||||
|
break;
|
||||||
|
case GravityDirection::RIGHT: // Impulsar IZQUIERDA
|
||||||
|
vx = -MAIN;
|
||||||
|
vy = LATERAL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ball->modVel(vx, vy); // Modifica la velocidad según dirección de gravedad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::switchBallsGravity() {
|
||||||
|
for (auto& ball : balls_) {
|
||||||
|
ball->switchGravity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::enableBallsGravityIfDisabled() {
|
||||||
|
for (auto& ball : balls_) {
|
||||||
|
ball->enableGravityIfDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::forceBallsGravityOn() {
|
||||||
|
for (auto& ball : balls_) {
|
||||||
|
ball->forceGravityOn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::forceBallsGravityOff() {
|
||||||
|
// Contar cuántas pelotas están en superficie (suelo/techo/pared)
|
||||||
|
int balls_on_surface = 0;
|
||||||
|
for (const auto& ball : balls_) {
|
||||||
|
if (ball->isOnSurface()) {
|
||||||
|
balls_on_surface++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si la mayoría (>50%) están en superficie, aplicar impulso para que se vea el efecto
|
||||||
|
float surface_ratio = static_cast<float>(balls_on_surface) / static_cast<float>(balls_.size());
|
||||||
|
if (surface_ratio > 0.5f) {
|
||||||
|
pushBallsAwayFromGravity(); // Dar impulso contrario a gravedad
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desactivar gravedad
|
||||||
|
for (auto& ball : balls_) {
|
||||||
|
ball->forceGravityOff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::changeGravityDirection(GravityDirection direction) {
|
||||||
|
current_gravity_ = direction;
|
||||||
|
for (auto& ball : balls_) {
|
||||||
|
ball->setGravityDirection(direction);
|
||||||
|
ball->applyRandomLateralPush(); // Aplicar empuje lateral aleatorio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::updateScreenSize(int width, int height) {
|
||||||
|
screen_width_ = width;
|
||||||
|
screen_height_ = height;
|
||||||
|
|
||||||
|
// NOTA: No actualizamos las bolas existentes, solo afecta a futuras creaciones
|
||||||
|
// Si se requiere reposicionar bolas existentes, implementar aquí
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Métodos privados ===
|
||||||
|
|
||||||
|
void SceneManager::updateBallSizes(int old_size, int new_size) {
|
||||||
|
for (auto& ball : balls_) {
|
||||||
|
SDL_FRect pos = ball->getPosition();
|
||||||
|
|
||||||
|
// Ajustar posición para compensar cambio de tamaño
|
||||||
|
// Si aumenta tamaño, mover hacia centro; si disminuye, alejar del centro
|
||||||
|
float center_x = screen_width_ / 2.0f;
|
||||||
|
float center_y = screen_height_ / 2.0f;
|
||||||
|
|
||||||
|
float dx = pos.x - center_x;
|
||||||
|
float dy = pos.y - center_y;
|
||||||
|
|
||||||
|
// Ajustar proporcionalmente (evitar divisiones por cero)
|
||||||
|
if (old_size > 0) {
|
||||||
|
float scale_factor = static_cast<float>(new_size) / static_cast<float>(old_size);
|
||||||
|
pos.x = center_x + dx * scale_factor;
|
||||||
|
pos.y = center_y + dy * scale_factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar tamaño del hitbox
|
||||||
|
ball->updateSize(new_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::enableShapeAttractionAll(bool enabled) {
|
||||||
|
for (auto& ball : balls_) {
|
||||||
|
ball->enableShapeAttraction(enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::resetDepthScalesAll() {
|
||||||
|
for (auto& ball : balls_) {
|
||||||
|
ball->setDepthScale(1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
185
source/scene/scene_manager.hpp
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory> // for unique_ptr, shared_ptr
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include "ball.hpp" // for Ball
|
||||||
|
#include "defines.hpp" // for GravityDirection
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
class Texture;
|
||||||
|
class ThemeManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class SceneManager
|
||||||
|
* @brief Gestiona toda la lógica de creación, física y actualización de bolas
|
||||||
|
*
|
||||||
|
* Responsabilidad única: Manejo de la escena (bolas, gravedad, física)
|
||||||
|
*
|
||||||
|
* Características:
|
||||||
|
* - Crea y destruye bolas según escenario seleccionado
|
||||||
|
* - Controla la dirección y estado de la gravedad
|
||||||
|
* - Actualiza física de todas las bolas cada frame
|
||||||
|
* - Proporciona acceso controlado a las bolas para rendering
|
||||||
|
* - Mantiene el Engine desacoplado de la lógica de física
|
||||||
|
*/
|
||||||
|
class SceneManager {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Constructor
|
||||||
|
* @param screen_width Ancho lógico de la pantalla
|
||||||
|
* @param screen_height Alto lógico de la pantalla
|
||||||
|
*/
|
||||||
|
SceneManager(int screen_width, int screen_height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Inicializa el manager con configuración inicial
|
||||||
|
* @param scenario Escenario inicial (índice de BALL_COUNT_SCENARIOS)
|
||||||
|
* @param texture Textura compartida para sprites de bolas
|
||||||
|
* @param theme_manager Puntero al gestor de temas (para colores)
|
||||||
|
*/
|
||||||
|
void initialize(int scenario, std::shared_ptr<Texture> texture, ThemeManager* theme_manager);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Actualiza física de todas las bolas
|
||||||
|
* @param delta_time Tiempo transcurrido desde último frame (segundos)
|
||||||
|
*/
|
||||||
|
void update(float delta_time);
|
||||||
|
|
||||||
|
// === Gestión de bolas ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Cambia el número de bolas según escenario
|
||||||
|
* @param scenario_id Índice del escenario (0-7 para 10 a 50,000 bolas; 8 = custom)
|
||||||
|
* @param mode Modo de simulación actual (afecta inicialización)
|
||||||
|
*/
|
||||||
|
void changeScenario(int scenario_id, SimulationMode mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Configura el número de bolas para el escenario custom (índice 8)
|
||||||
|
* @param n Número de bolas del escenario custom
|
||||||
|
*/
|
||||||
|
void setCustomBallCount(int n) { custom_ball_count_ = n; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Actualiza textura y tamaño de todas las bolas
|
||||||
|
* @param new_texture Nueva textura compartida
|
||||||
|
* @param new_ball_size Nuevo tamaño de bolas (píxeles)
|
||||||
|
*/
|
||||||
|
void updateBallTexture(std::shared_ptr<Texture> new_texture, int new_ball_size);
|
||||||
|
|
||||||
|
// === Control de gravedad ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Aplica impulso a todas las bolas alejándolas de la superficie de gravedad
|
||||||
|
*/
|
||||||
|
void pushBallsAwayFromGravity();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Alterna el estado de gravedad (ON/OFF) en todas las bolas
|
||||||
|
*/
|
||||||
|
void switchBallsGravity();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reactiva gravedad solo si estaba desactivada
|
||||||
|
*/
|
||||||
|
void enableBallsGravityIfDisabled();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Fuerza gravedad ON en todas las bolas
|
||||||
|
*/
|
||||||
|
void forceBallsGravityOn();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Fuerza gravedad OFF en todas las bolas (con impulso si >50% en superficie)
|
||||||
|
*/
|
||||||
|
void forceBallsGravityOff();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Cambia la dirección de la gravedad
|
||||||
|
* @param direction Nueva dirección (UP/DOWN/LEFT/RIGHT)
|
||||||
|
*/
|
||||||
|
void changeGravityDirection(GravityDirection direction);
|
||||||
|
|
||||||
|
// === Acceso a datos (read-only) ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Obtiene referencia constante al vector de bolas (para rendering)
|
||||||
|
*/
|
||||||
|
const std::vector<std::unique_ptr<Ball>>& getBalls() const { return balls_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Obtiene referencia mutable al vector de bolas (para ShapeManager/BoidManager)
|
||||||
|
* NOTA: Usar con cuidado, solo para sistemas que necesitan modificar estado de bolas
|
||||||
|
*/
|
||||||
|
std::vector<std::unique_ptr<Ball>>& getBallsMutable() { return balls_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Activa o desactiva la atracción de figura en todas las bolas
|
||||||
|
* @param enabled true para activar, false para desactivar
|
||||||
|
*/
|
||||||
|
void enableShapeAttractionAll(bool enabled);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Resetea la escala de profundidad a 1.0 en todas las bolas
|
||||||
|
*/
|
||||||
|
void resetDepthScalesAll();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Obtiene número total de bolas
|
||||||
|
*/
|
||||||
|
size_t getBallCount() const { return balls_.size(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Verifica si hay al menos una bola
|
||||||
|
*/
|
||||||
|
bool hasBalls() const { return !balls_.empty(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Obtiene puntero a la primera bola (para debug info)
|
||||||
|
* @return Puntero constante o nullptr si no hay bolas
|
||||||
|
*/
|
||||||
|
const Ball* getFirstBall() const { return balls_.empty() ? nullptr : balls_[0].get(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Obtiene dirección actual de gravedad
|
||||||
|
*/
|
||||||
|
GravityDirection getCurrentGravity() const { return current_gravity_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Obtiene escenario actual
|
||||||
|
*/
|
||||||
|
int getCurrentScenario() const { return scenario_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Actualiza resolución de pantalla (para resize/fullscreen)
|
||||||
|
* @param width Nuevo ancho lógico
|
||||||
|
* @param height Nuevo alto lógico
|
||||||
|
*/
|
||||||
|
void updateScreenSize(int width, int height);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// === Datos de escena ===
|
||||||
|
std::vector<std::unique_ptr<Ball>> balls_;
|
||||||
|
GravityDirection current_gravity_;
|
||||||
|
int scenario_;
|
||||||
|
int custom_ball_count_ = 0; // Número de bolas para escenario custom (índice 8)
|
||||||
|
|
||||||
|
// === Configuración de pantalla ===
|
||||||
|
int screen_width_;
|
||||||
|
int screen_height_;
|
||||||
|
int current_ball_size_;
|
||||||
|
|
||||||
|
// === Referencias a otros sistemas (no owned) ===
|
||||||
|
std::shared_ptr<Texture> texture_;
|
||||||
|
ThemeManager* theme_manager_;
|
||||||
|
|
||||||
|
// === Métodos privados auxiliares ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Ajusta posiciones de bolas al cambiar tamaño de sprite
|
||||||
|
* @param old_size Tamaño anterior
|
||||||
|
* @param new_size Tamaño nuevo
|
||||||
|
*/
|
||||||
|
void updateBallSizes(int old_size, int new_size);
|
||||||
|
};
|
||||||
101
source/shapes/atom_shape.cpp
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#include "atom_shape.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "defines.hpp"
|
||||||
|
|
||||||
|
void AtomShape::generatePoints(int num_points, float screen_width, float screen_height) {
|
||||||
|
num_points_ = num_points;
|
||||||
|
nucleus_radius_ = screen_height * ATOM_NUCLEUS_RADIUS_FACTOR;
|
||||||
|
orbit_radius_ = screen_height * ATOM_ORBIT_RADIUS_FACTOR;
|
||||||
|
// Las posiciones se calculan en getPoint3D()
|
||||||
|
}
|
||||||
|
|
||||||
|
void AtomShape::update(float delta_time, float screen_width, float screen_height) {
|
||||||
|
// Recalcular dimensiones por si cambió resolución (F4)
|
||||||
|
nucleus_radius_ = screen_height * ATOM_NUCLEUS_RADIUS_FACTOR;
|
||||||
|
orbit_radius_ = screen_height * ATOM_ORBIT_RADIUS_FACTOR;
|
||||||
|
|
||||||
|
// Actualizar rotación global del átomo
|
||||||
|
angle_y_ += ATOM_ROTATION_SPEED_Y * delta_time;
|
||||||
|
|
||||||
|
// Actualizar fase de rotación de electrones en órbitas
|
||||||
|
orbit_phase_ += ATOM_ORBIT_ROTATION_SPEED * delta_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AtomShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||||
|
int num_orbits = static_cast<int>(ATOM_NUM_ORBITS);
|
||||||
|
|
||||||
|
// Calcular cuántos puntos para núcleo vs órbitas
|
||||||
|
int nucleus_points = (num_points_ < 10) ? 1 : (num_points_ / 10); // 10% para núcleo
|
||||||
|
nucleus_points = std::max(nucleus_points, 1);
|
||||||
|
|
||||||
|
// Si estamos en el núcleo
|
||||||
|
if (index < nucleus_points) {
|
||||||
|
// Distribuir puntos en esfera pequeña (núcleo)
|
||||||
|
float t = static_cast<float>(index) / static_cast<float>(nucleus_points);
|
||||||
|
float phi = acosf(1.0f - (2.0f * t));
|
||||||
|
float theta = PI * 2.0f * t * 3.61803398875f; // Golden ratio
|
||||||
|
|
||||||
|
float x_nuc = nucleus_radius_ * cosf(theta) * sinf(phi);
|
||||||
|
float y_nuc = nucleus_radius_ * sinf(theta) * sinf(phi);
|
||||||
|
float z_nuc = nucleus_radius_ * cosf(phi);
|
||||||
|
|
||||||
|
// Aplicar rotación global
|
||||||
|
float cos_y = cosf(angle_y_);
|
||||||
|
float sin_y = sinf(angle_y_);
|
||||||
|
x = x_nuc * cos_y - z_nuc * sin_y;
|
||||||
|
y = y_nuc;
|
||||||
|
z = x_nuc * sin_y + z_nuc * cos_y;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Puntos restantes: distribuir en órbitas
|
||||||
|
int orbit_points = num_points_ - nucleus_points;
|
||||||
|
int points_per_orbit = orbit_points / num_orbits;
|
||||||
|
points_per_orbit = std::max(points_per_orbit, 1);
|
||||||
|
|
||||||
|
int orbit_index = (index - nucleus_points) / points_per_orbit;
|
||||||
|
if (orbit_index >= num_orbits) {
|
||||||
|
orbit_index = num_orbits - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int point_in_orbit = (index - nucleus_points) % points_per_orbit;
|
||||||
|
|
||||||
|
// Ángulo del electrón en su órbita
|
||||||
|
float electron_angle = (static_cast<float>(point_in_orbit) / static_cast<float>(points_per_orbit)) * 2.0f * PI;
|
||||||
|
electron_angle += orbit_phase_; // Añadir rotación animada
|
||||||
|
|
||||||
|
// Inclinación del plano orbital (cada órbita en ángulo diferente)
|
||||||
|
float orbit_tilt = (static_cast<float>(orbit_index) / static_cast<float>(num_orbits)) * PI;
|
||||||
|
|
||||||
|
// Posición del electrón en su órbita (plano XY local)
|
||||||
|
float x_local = orbit_radius_ * cosf(electron_angle);
|
||||||
|
float y_local = orbit_radius_ * sinf(electron_angle);
|
||||||
|
float z_local = 0.0f;
|
||||||
|
|
||||||
|
// Inclinar el plano orbital (rotación en eje X local)
|
||||||
|
float cos_tilt = cosf(orbit_tilt);
|
||||||
|
float sin_tilt = sinf(orbit_tilt);
|
||||||
|
float y_tilted = (y_local * cos_tilt) - (z_local * sin_tilt);
|
||||||
|
float z_tilted = (y_local * sin_tilt) + (z_local * cos_tilt);
|
||||||
|
|
||||||
|
// Aplicar rotación global del átomo (eje Y)
|
||||||
|
float cos_y = cosf(angle_y_);
|
||||||
|
float sin_y = sinf(angle_y_);
|
||||||
|
float x_rot = (x_local * cos_y) - (z_tilted * sin_y);
|
||||||
|
float z_rot = (x_local * sin_y) + (z_tilted * cos_y);
|
||||||
|
|
||||||
|
x = x_rot;
|
||||||
|
y = y_tilted;
|
||||||
|
z = z_rot;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto AtomShape::getScaleFactor(float screen_height) const -> float {
|
||||||
|
// Factor de escala para física: proporcional al radio de órbita
|
||||||
|
// Radio órbita base = 72px (0.30 * 240px en resolución 320x240)
|
||||||
|
const float BASE_RADIUS = 72.0f;
|
||||||
|
float current_radius = screen_height * ATOM_ORBIT_RADIUS_FACTOR;
|
||||||
|
return current_radius / BASE_RADIUS;
|
||||||
|
}
|
||||||
22
source/shapes/atom_shape.hpp
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "shape.hpp"
|
||||||
|
|
||||||
|
// Figura: Átomo con núcleo central y órbitas electrónicas
|
||||||
|
// Comportamiento: Núcleo estático + electrones orbitando en planos inclinados
|
||||||
|
// Efecto: Modelo atómico clásico Bohr
|
||||||
|
class AtomShape : public Shape {
|
||||||
|
private:
|
||||||
|
float angle_y_ = 0.0f; // Ángulo de rotación global en eje Y (rad)
|
||||||
|
float orbit_phase_ = 0.0f; // Fase de rotación de electrones (rad)
|
||||||
|
float nucleus_radius_ = 0.0f; // Radio del núcleo central (píxeles)
|
||||||
|
float orbit_radius_ = 0.0f; // Radio de las órbitas (píxeles)
|
||||||
|
int num_points_ = 0; // Cantidad total de puntos
|
||||||
|
|
||||||
|
public:
|
||||||
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
const char* getName() const override { return "ATOM"; }
|
||||||
|
float getScaleFactor(float screen_height) const override;
|
||||||
|
};
|
||||||
183
source/shapes/cube_shape.cpp
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
#include "cube_shape.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "defines.hpp"
|
||||||
|
|
||||||
|
void CubeShape::generatePoints(int num_points, float screen_width, float screen_height) {
|
||||||
|
num_points_ = num_points;
|
||||||
|
size_ = screen_height * CUBE_SIZE_FACTOR;
|
||||||
|
|
||||||
|
// Limpiar vectores anteriores
|
||||||
|
base_x_.clear();
|
||||||
|
base_y_.clear();
|
||||||
|
base_z_.clear();
|
||||||
|
|
||||||
|
// Seleccionar estrategia según cantidad de pelotas
|
||||||
|
if (num_points <= 8) {
|
||||||
|
generateVertices();
|
||||||
|
} else if (num_points <= 26) {
|
||||||
|
generateVerticesAndCenters();
|
||||||
|
} else {
|
||||||
|
generateVolumetricGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si sobran posiciones, repetir en espiral (distribución uniforme)
|
||||||
|
while (static_cast<int>(base_x_.size()) < num_points) {
|
||||||
|
base_x_.push_back(base_x_[base_x_.size() % base_x_.size()]);
|
||||||
|
base_y_.push_back(base_y_[base_y_.size() % base_y_.size()]);
|
||||||
|
base_z_.push_back(base_z_[base_z_.size() % base_z_.size()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CubeShape::update(float delta_time, float screen_width, float screen_height) {
|
||||||
|
// Recalcular tamaño por si cambió resolución (F4)
|
||||||
|
size_ = screen_height * CUBE_SIZE_FACTOR;
|
||||||
|
|
||||||
|
// Actualizar ángulos de rotación en los 3 ejes (efecto Rubik)
|
||||||
|
angle_x_ += CUBE_ROTATION_SPEED_X * delta_time;
|
||||||
|
angle_y_ += CUBE_ROTATION_SPEED_Y * delta_time;
|
||||||
|
angle_z_ += CUBE_ROTATION_SPEED_Z * delta_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CubeShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||||
|
if (index >= static_cast<int>(base_x_.size())) {
|
||||||
|
x = y = z = 0.0f;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener posición base
|
||||||
|
float x_base = base_x_[index];
|
||||||
|
float y_base = base_y_[index];
|
||||||
|
float z_base = base_z_[index];
|
||||||
|
|
||||||
|
// Aplicar rotación en eje Z
|
||||||
|
float cos_z = cosf(angle_z_);
|
||||||
|
float sin_z = sinf(angle_z_);
|
||||||
|
float x_rot_z = (x_base * cos_z) - (y_base * sin_z);
|
||||||
|
float y_rot_z = (x_base * sin_z) + (y_base * cos_z);
|
||||||
|
float z_rot_z = z_base;
|
||||||
|
|
||||||
|
// Aplicar rotación en eje Y
|
||||||
|
float cos_y = cosf(angle_y_);
|
||||||
|
float sin_y = sinf(angle_y_);
|
||||||
|
float x_rot_y = (x_rot_z * cos_y) + (z_rot_z * sin_y);
|
||||||
|
float y_rot_y = y_rot_z;
|
||||||
|
float z_rot_y = (-x_rot_z * sin_y) + (z_rot_z * cos_y);
|
||||||
|
|
||||||
|
// Aplicar rotación en eje X
|
||||||
|
float cos_x = cosf(angle_x_);
|
||||||
|
float sin_x = sinf(angle_x_);
|
||||||
|
float x_final = x_rot_y;
|
||||||
|
float y_final = (y_rot_y * cos_x) - (z_rot_y * sin_x);
|
||||||
|
float z_final = (y_rot_y * sin_x) + (z_rot_y * cos_x);
|
||||||
|
|
||||||
|
// Retornar coordenadas finales rotadas
|
||||||
|
x = x_final;
|
||||||
|
y = y_final;
|
||||||
|
z = z_final;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CubeShape::getScaleFactor(float screen_height) const -> float {
|
||||||
|
// Factor de escala para física: proporcional al tamaño del cubo
|
||||||
|
// Tamaño base = 60px (resolución 320x240, factor 0.25)
|
||||||
|
const float BASE_SIZE = 60.0f;
|
||||||
|
float current_size = screen_height * CUBE_SIZE_FACTOR;
|
||||||
|
return current_size / BASE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Métodos auxiliares privados
|
||||||
|
|
||||||
|
void CubeShape::generateVertices() {
|
||||||
|
// 8 vértices del cubo: todas las combinaciones de (±size, ±size, ±size)
|
||||||
|
for (int x_sign : {-1, 1}) {
|
||||||
|
for (int y_sign : {-1, 1}) {
|
||||||
|
for (int z_sign : {-1, 1}) {
|
||||||
|
base_x_.push_back(x_sign * size_);
|
||||||
|
base_y_.push_back(y_sign * size_);
|
||||||
|
base_z_.push_back(z_sign * size_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CubeShape::generateVerticesAndCenters() {
|
||||||
|
// 1. Añadir 8 vértices
|
||||||
|
generateVertices();
|
||||||
|
|
||||||
|
// 2. Añadir 6 centros de caras
|
||||||
|
// Caras: X=±size (Y,Z varían), Y=±size (X,Z varían), Z=±size (X,Y varían)
|
||||||
|
base_x_.push_back(size_);
|
||||||
|
base_y_.push_back(0);
|
||||||
|
base_z_.push_back(0); // +X
|
||||||
|
base_x_.push_back(-size_);
|
||||||
|
base_y_.push_back(0);
|
||||||
|
base_z_.push_back(0); // -X
|
||||||
|
base_x_.push_back(0);
|
||||||
|
base_y_.push_back(size_);
|
||||||
|
base_z_.push_back(0); // +Y
|
||||||
|
base_x_.push_back(0);
|
||||||
|
base_y_.push_back(-size_);
|
||||||
|
base_z_.push_back(0); // -Y
|
||||||
|
base_x_.push_back(0);
|
||||||
|
base_y_.push_back(0);
|
||||||
|
base_z_.push_back(size_); // +Z
|
||||||
|
base_x_.push_back(0);
|
||||||
|
base_y_.push_back(0);
|
||||||
|
base_z_.push_back(-size_); // -Z
|
||||||
|
|
||||||
|
// 3. Añadir 12 centros de aristas
|
||||||
|
// Aristas paralelas a X (4), Y (4), Z (4)
|
||||||
|
// Paralelas a X (Y y Z en vértices, X=0)
|
||||||
|
for (int y_sign : {-1, 1}) {
|
||||||
|
for (int z_sign : {-1, 1}) {
|
||||||
|
base_x_.push_back(0);
|
||||||
|
base_y_.push_back(y_sign * size_);
|
||||||
|
base_z_.push_back(z_sign * size_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Paralelas a Y (X y Z en vértices, Y=0)
|
||||||
|
for (int x_sign : {-1, 1}) {
|
||||||
|
for (int z_sign : {-1, 1}) {
|
||||||
|
base_x_.push_back(x_sign * size_);
|
||||||
|
base_y_.push_back(0);
|
||||||
|
base_z_.push_back(z_sign * size_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Paralelas a Z (X y Y en vértices, Z=0)
|
||||||
|
for (int x_sign : {-1, 1}) {
|
||||||
|
for (int y_sign : {-1, 1}) {
|
||||||
|
base_x_.push_back(x_sign * size_);
|
||||||
|
base_y_.push_back(y_sign * size_);
|
||||||
|
base_z_.push_back(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CubeShape::generateVolumetricGrid() {
|
||||||
|
// Calcular dimensión del grid cúbico: N³ ≈ num_points
|
||||||
|
int grid_dim = static_cast<int>(ceilf(cbrtf(static_cast<float>(num_points_))));
|
||||||
|
grid_dim = std::max(grid_dim, 3); // Mínimo grid 3x3x3
|
||||||
|
|
||||||
|
float step = (2.0f * size_) / (grid_dim - 1); // Espacio entre puntos
|
||||||
|
|
||||||
|
for (int ix = 0; ix < grid_dim; ix++) {
|
||||||
|
for (int iy = 0; iy < grid_dim; iy++) {
|
||||||
|
for (int iz = 0; iz < grid_dim; iz++) {
|
||||||
|
float x = -size_ + (ix * step);
|
||||||
|
float y = -size_ + (iy * step);
|
||||||
|
float z = -size_ + (iz * step);
|
||||||
|
|
||||||
|
base_x_.push_back(x);
|
||||||
|
base_y_.push_back(y);
|
||||||
|
base_z_.push_back(z);
|
||||||
|
|
||||||
|
// Si ya tenemos suficientes puntos, salir
|
||||||
|
if (static_cast<int>(base_x_.size()) >= num_points_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
source/shapes/cube_shape.hpp
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "shape.hpp"
|
||||||
|
|
||||||
|
// Figura: Cubo 3D rotante
|
||||||
|
// Distribución:
|
||||||
|
// - 1-8 pelotas: Solo vértices (8 puntos)
|
||||||
|
// - 9-26 pelotas: Vértices + centros de caras + centros de aristas (26 puntos)
|
||||||
|
// - 27+ pelotas: Grid volumétrico 3D uniforme
|
||||||
|
// Comportamiento: Rotación simultánea en ejes X, Y, Z (efecto Rubik)
|
||||||
|
class CubeShape : public Shape {
|
||||||
|
private:
|
||||||
|
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
|
||||||
|
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
||||||
|
float angle_z_ = 0.0f; // Ángulo de rotación en eje Z (rad)
|
||||||
|
float size_ = 0.0f; // Mitad del lado del cubo (píxeles)
|
||||||
|
int num_points_ = 0; // Cantidad de puntos generados
|
||||||
|
|
||||||
|
// Posiciones base 3D (sin rotar) - se calculan en generatePoints()
|
||||||
|
std::vector<float> base_x_;
|
||||||
|
std::vector<float> base_y_;
|
||||||
|
std::vector<float> base_z_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
const char* getName() const override { return "CUBE"; }
|
||||||
|
float getScaleFactor(float screen_height) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Métodos auxiliares para distribución de puntos
|
||||||
|
void generateVertices(); // 8 vértices
|
||||||
|
void generateVerticesAndCenters(); // 26 puntos (vértices + caras + aristas)
|
||||||
|
void generateVolumetricGrid(); // Grid 3D para muchas pelotas
|
||||||
|
};
|
||||||
123
source/shapes/cylinder_shape.cpp
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
#include "cylinder_shape.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib> // Para rand()
|
||||||
|
|
||||||
|
#include "defines.hpp"
|
||||||
|
|
||||||
|
void CylinderShape::generatePoints(int num_points, float screen_width, float screen_height) {
|
||||||
|
num_points_ = num_points;
|
||||||
|
radius_ = screen_height * CYLINDER_RADIUS_FACTOR;
|
||||||
|
height_ = screen_height * CYLINDER_HEIGHT_FACTOR;
|
||||||
|
|
||||||
|
// Inicializar timer de tumbling con valor aleatorio (3-5 segundos)
|
||||||
|
tumble_timer_ = 3.0f + (rand() % 2000) / 1000.0f;
|
||||||
|
// Las posiciones 3D se calculan en getPoint3D() usando ecuaciones paramétricas del cilindro
|
||||||
|
}
|
||||||
|
|
||||||
|
void CylinderShape::update(float delta_time, float screen_width, float screen_height) {
|
||||||
|
// Recalcular dimensiones por si cambió resolución (F4)
|
||||||
|
radius_ = screen_height * CYLINDER_RADIUS_FACTOR;
|
||||||
|
height_ = screen_height * CYLINDER_HEIGHT_FACTOR;
|
||||||
|
|
||||||
|
// Actualizar ángulo de rotación en eje Y (siempre activo)
|
||||||
|
angle_y_ += CYLINDER_ROTATION_SPEED_Y * delta_time;
|
||||||
|
|
||||||
|
// Sistema de tumbling ocasional
|
||||||
|
if (is_tumbling_) {
|
||||||
|
// Estamos en tumble: animar angle_x hacia el objetivo
|
||||||
|
tumble_duration_ += delta_time;
|
||||||
|
float tumble_progress = tumble_duration_ / 1.5f; // 1.5 segundos de duración
|
||||||
|
|
||||||
|
if (tumble_progress >= 1.0f) {
|
||||||
|
// Tumble completado
|
||||||
|
angle_x_ = tumble_target_;
|
||||||
|
is_tumbling_ = false;
|
||||||
|
tumble_timer_ = 3.0f + (rand() % 2000) / 1000.0f; // Nuevo timer (3-5s)
|
||||||
|
} else {
|
||||||
|
// Interpolación suave con ease-in-out
|
||||||
|
float t = tumble_progress;
|
||||||
|
float ease = t < 0.5f
|
||||||
|
? 2.0f * t * t
|
||||||
|
: 1.0f - ((-2.0f * t + 2.0f) * (-2.0f * t + 2.0f) / 2.0f);
|
||||||
|
angle_x_ = ease * tumble_target_;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No estamos en tumble: contar tiempo
|
||||||
|
tumble_timer_ -= delta_time;
|
||||||
|
if (tumble_timer_ <= 0.0f) {
|
||||||
|
// Iniciar nuevo tumble
|
||||||
|
is_tumbling_ = true;
|
||||||
|
tumble_duration_ = 0.0f;
|
||||||
|
// Objetivo: PI/2 radianes (90°) o -PI/2
|
||||||
|
tumble_target_ = angle_x_ + ((rand() % 2) == 0 ? PI * 0.5f : -PI * 0.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CylinderShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||||
|
// Distribuir puntos uniformemente en la superficie del cilindro
|
||||||
|
// Calcular número de anillos (altura) y puntos por anillo (circunferencia)
|
||||||
|
|
||||||
|
int num_rings = static_cast<int>(sqrtf(static_cast<float>(num_points_) * 0.5f));
|
||||||
|
num_rings = std::max(num_rings, 2);
|
||||||
|
|
||||||
|
int points_per_ring = num_points_ / num_rings;
|
||||||
|
points_per_ring = std::max(points_per_ring, 3);
|
||||||
|
|
||||||
|
// Obtener parámetros u (ángulo) y v (altura) del índice
|
||||||
|
int ring = index / points_per_ring;
|
||||||
|
int point_in_ring = index % points_per_ring;
|
||||||
|
|
||||||
|
// Si nos pasamos del número de anillos, usar el último
|
||||||
|
if (ring >= num_rings) {
|
||||||
|
ring = num_rings - 1;
|
||||||
|
point_in_ring = index - (ring * points_per_ring);
|
||||||
|
if (point_in_ring >= points_per_ring) {
|
||||||
|
point_in_ring = points_per_ring - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parámetro u (ángulo alrededor del cilindro): [0, 2π]
|
||||||
|
float u = (static_cast<float>(point_in_ring) / static_cast<float>(points_per_ring)) * 2.0f * PI;
|
||||||
|
|
||||||
|
// Parámetro v (altura normalizada): [-1, 1]
|
||||||
|
float v = ((static_cast<float>(ring) / static_cast<float>(num_rings - 1)) * 2.0f) - 1.0f;
|
||||||
|
if (num_rings == 1) {
|
||||||
|
v = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ecuaciones paramétricas del cilindro
|
||||||
|
// x = radius * cos(u)
|
||||||
|
// y = height * v
|
||||||
|
// z = radius * sin(u)
|
||||||
|
float x_base = radius_ * cosf(u);
|
||||||
|
float y_base = (height_ * 0.5f) * v; // Centrar verticalmente
|
||||||
|
float z_base = radius_ * sinf(u);
|
||||||
|
|
||||||
|
// Aplicar rotación en eje Y (principal, siempre activa)
|
||||||
|
float cos_y = cosf(angle_y_);
|
||||||
|
float sin_y = sinf(angle_y_);
|
||||||
|
float x_rot_y = (x_base * cos_y) - (z_base * sin_y);
|
||||||
|
float z_rot_y = (x_base * sin_y) + (z_base * cos_y);
|
||||||
|
|
||||||
|
// Aplicar rotación en eje X (tumbling ocasional)
|
||||||
|
float cos_x = cosf(angle_x_);
|
||||||
|
float sin_x = sinf(angle_x_);
|
||||||
|
float y_rot = (y_base * cos_x) - (z_rot_y * sin_x);
|
||||||
|
float z_rot = (y_base * sin_x) + (z_rot_y * cos_x);
|
||||||
|
|
||||||
|
// Retornar coordenadas finales con ambas rotaciones
|
||||||
|
x = x_rot_y;
|
||||||
|
y = y_rot;
|
||||||
|
z = z_rot;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CylinderShape::getScaleFactor(float screen_height) const -> float {
|
||||||
|
// Factor de escala para física: proporcional a la dimensión mayor (altura)
|
||||||
|
// Altura base = 120px (0.5 * 240px en resolución 320x240)
|
||||||
|
const float BASE_HEIGHT = 120.0f;
|
||||||
|
float current_height = screen_height * CYLINDER_HEIGHT_FACTOR;
|
||||||
|
return current_height / BASE_HEIGHT;
|
||||||
|
}
|
||||||
28
source/shapes/cylinder_shape.hpp
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "shape.hpp"
|
||||||
|
|
||||||
|
// Figura: Cilindro 3D rotante
|
||||||
|
// Comportamiento: Superficie cilíndrica con rotación en eje Y + tumbling ocasional en X/Z
|
||||||
|
// Ecuaciones: x = r*cos(u), y = v, z = r*sin(u)
|
||||||
|
class CylinderShape : public Shape {
|
||||||
|
private:
|
||||||
|
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
||||||
|
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (tumbling ocasional)
|
||||||
|
float radius_ = 0.0f; // Radio del cilindro (píxeles)
|
||||||
|
float height_ = 0.0f; // Altura del cilindro (píxeles)
|
||||||
|
int num_points_ = 0; // Cantidad de puntos generados
|
||||||
|
|
||||||
|
// Sistema de tumbling ocasional
|
||||||
|
float tumble_timer_ = 0.0f; // Temporizador para próximo tumble
|
||||||
|
float tumble_duration_ = 0.0f; // Duración del tumble actual
|
||||||
|
bool is_tumbling_ = false; // ¿Estamos en modo tumble?
|
||||||
|
float tumble_target_ = 0.0f; // Ángulo objetivo del tumble
|
||||||
|
|
||||||
|
public:
|
||||||
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
const char* getName() const override { return "CYLINDER"; }
|
||||||
|
float getScaleFactor(float screen_height) const override;
|
||||||
|
};
|
||||||
61
source/shapes/helix_shape.cpp
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#include "helix_shape.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "defines.hpp"
|
||||||
|
|
||||||
|
void HelixShape::generatePoints(int num_points, float screen_width, float screen_height) {
|
||||||
|
num_points_ = num_points;
|
||||||
|
radius_ = screen_height * HELIX_RADIUS_FACTOR;
|
||||||
|
pitch_ = screen_height * HELIX_PITCH_FACTOR;
|
||||||
|
total_height_ = pitch_ * HELIX_NUM_TURNS;
|
||||||
|
// Las posiciones 3D se calculan en getPoint3D() usando ecuaciones paramétricas
|
||||||
|
}
|
||||||
|
|
||||||
|
void HelixShape::update(float delta_time, float screen_width, float screen_height) {
|
||||||
|
// Recalcular dimensiones por si cambió resolución (F4)
|
||||||
|
radius_ = screen_height * HELIX_RADIUS_FACTOR;
|
||||||
|
pitch_ = screen_height * HELIX_PITCH_FACTOR;
|
||||||
|
total_height_ = pitch_ * HELIX_NUM_TURNS;
|
||||||
|
|
||||||
|
// Actualizar rotación en eje Y (horizontal)
|
||||||
|
angle_y_ += HELIX_ROTATION_SPEED_Y * delta_time;
|
||||||
|
|
||||||
|
// Actualizar fase para animación vertical (efecto "subiendo/bajando")
|
||||||
|
phase_offset_ += HELIX_PHASE_SPEED * delta_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HelixShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||||
|
// Parámetro t: distribuir uniformemente de 0 a (2π * num_turns)
|
||||||
|
float t = (static_cast<float>(index) / static_cast<float>(num_points_)) * (2.0f * PI * HELIX_NUM_TURNS);
|
||||||
|
|
||||||
|
// Ecuaciones paramétricas de hélice
|
||||||
|
// x = radius * cos(t)
|
||||||
|
// y = pitch * (t / 2π) + phase_offset (altura proporcional al ángulo)
|
||||||
|
// z = radius * sin(t)
|
||||||
|
float x_base = radius_ * cosf(t);
|
||||||
|
float y_base = (pitch_ * (t / (2.0f * PI))) + (sinf(phase_offset_) * pitch_ * 0.3f);
|
||||||
|
float z_base = radius_ * sinf(t);
|
||||||
|
|
||||||
|
// Centrar verticalmente: restar mitad de altura total
|
||||||
|
y_base -= total_height_ * 0.5f;
|
||||||
|
|
||||||
|
// Aplicar rotación en eje Y (horizontal)
|
||||||
|
float cos_y = cosf(angle_y_);
|
||||||
|
float sin_y = sinf(angle_y_);
|
||||||
|
float x_rot = (x_base * cos_y) - (z_base * sin_y);
|
||||||
|
float z_rot = (x_base * sin_y) + (z_base * cos_y);
|
||||||
|
|
||||||
|
// Retornar coordenadas finales
|
||||||
|
x = x_rot;
|
||||||
|
y = y_base;
|
||||||
|
z = z_rot;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto HelixShape::getScaleFactor(float screen_height) const -> float {
|
||||||
|
// Factor de escala para física: proporcional a la dimensión mayor (altura total)
|
||||||
|
// Altura base = 180px para 3 vueltas con pitch=0.25 en 240px de altura (180 = 240 * 0.25 * 3)
|
||||||
|
const float BASE_HEIGHT = 180.0f;
|
||||||
|
float current_height = screen_height * HELIX_PITCH_FACTOR * HELIX_NUM_TURNS;
|
||||||
|
return current_height / BASE_HEIGHT;
|
||||||
|
}
|
||||||
23
source/shapes/helix_shape.hpp
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "shape.hpp"
|
||||||
|
|
||||||
|
// Figura: Espiral helicoidal 3D con distribución uniforme
|
||||||
|
// Comportamiento: Rotación en eje Y + animación de fase vertical
|
||||||
|
// Ecuaciones: x = r*cos(t), y = pitch*t + phase, z = r*sin(t)
|
||||||
|
class HelixShape : public Shape {
|
||||||
|
private:
|
||||||
|
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
||||||
|
float phase_offset_ = 0.0f; // Offset de fase para animación vertical (rad)
|
||||||
|
float radius_ = 0.0f; // Radio de la espiral (píxeles)
|
||||||
|
float pitch_ = 0.0f; // Separación vertical entre vueltas (píxeles)
|
||||||
|
float total_height_ = 0.0f; // Altura total de la espiral (píxeles)
|
||||||
|
int num_points_ = 0; // Cantidad de puntos generados
|
||||||
|
|
||||||
|
public:
|
||||||
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
const char* getName() const override { return "HELIX"; }
|
||||||
|
float getScaleFactor(float screen_height) const override;
|
||||||
|
};
|
||||||
171
source/shapes/icosahedron_shape.cpp
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
#include "icosahedron_shape.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "defines.hpp"
|
||||||
|
|
||||||
|
void IcosahedronShape::generatePoints(int num_points, float screen_width, float screen_height) {
|
||||||
|
num_points_ = num_points;
|
||||||
|
radius_ = screen_height * ICOSAHEDRON_RADIUS_FACTOR;
|
||||||
|
// Los 12 vértices del icosaedro se calculan en getPoint3D()
|
||||||
|
}
|
||||||
|
|
||||||
|
void IcosahedronShape::update(float delta_time, float screen_width, float screen_height) {
|
||||||
|
// Recalcular radio por si cambió resolución (F4)
|
||||||
|
radius_ = screen_height * ICOSAHEDRON_RADIUS_FACTOR;
|
||||||
|
|
||||||
|
// Actualizar ángulos de rotación (triple rotación XYZ)
|
||||||
|
angle_x_ += ICOSAHEDRON_ROTATION_SPEED_X * delta_time;
|
||||||
|
angle_y_ += ICOSAHEDRON_ROTATION_SPEED_Y * delta_time;
|
||||||
|
angle_z_ += ICOSAHEDRON_ROTATION_SPEED_Z * delta_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IcosahedronShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||||
|
// Proporción áurea (golden ratio)
|
||||||
|
const float PHI = (1.0f + sqrtf(5.0f)) / 2.0f;
|
||||||
|
|
||||||
|
// 12 vértices del icosaedro regular normalizado
|
||||||
|
// Basados en 3 rectángulos áureos ortogonales
|
||||||
|
const std::array<std::array<float, 3>, 12> VERTICES = {{
|
||||||
|
// Rectángulo XY
|
||||||
|
{-1.0f, PHI, 0.0f},
|
||||||
|
{1.0f, PHI, 0.0f},
|
||||||
|
{-1.0f, -PHI, 0.0f},
|
||||||
|
{1.0f, -PHI, 0.0f},
|
||||||
|
// Rectángulo YZ
|
||||||
|
{0.0f, -1.0f, PHI},
|
||||||
|
{0.0f, 1.0f, PHI},
|
||||||
|
{0.0f, -1.0f, -PHI},
|
||||||
|
{0.0f, 1.0f, -PHI},
|
||||||
|
// Rectángulo ZX
|
||||||
|
{PHI, 0.0f, -1.0f},
|
||||||
|
{PHI, 0.0f, 1.0f},
|
||||||
|
{-PHI, 0.0f, -1.0f},
|
||||||
|
{-PHI, 0.0f, 1.0f}}};
|
||||||
|
|
||||||
|
// Normalizar para esfera circunscrita
|
||||||
|
const float NORMALIZATION = sqrtf(1.0f + (PHI * PHI));
|
||||||
|
|
||||||
|
// Si tenemos 12 o menos puntos, usar solo vértices
|
||||||
|
if (num_points_ <= 12) {
|
||||||
|
int vertex_index = index % 12;
|
||||||
|
float x_base = VERTICES[vertex_index][0] / NORMALIZATION * radius_;
|
||||||
|
float y_base = VERTICES[vertex_index][1] / NORMALIZATION * radius_;
|
||||||
|
float z_base = VERTICES[vertex_index][2] / NORMALIZATION * radius_;
|
||||||
|
|
||||||
|
// Aplicar rotaciones
|
||||||
|
applyRotations(x_base, y_base, z_base, x, y, z);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para más de 12 puntos: subdividir caras triangulares
|
||||||
|
// Distribuir puntos entre vértices (primero) y caras (después)
|
||||||
|
if (index < 12) {
|
||||||
|
// Primeros 12 puntos: vértices del icosaedro
|
||||||
|
float x_base = VERTICES[index][0] / NORMALIZATION * radius_;
|
||||||
|
float y_base = VERTICES[index][1] / NORMALIZATION * radius_;
|
||||||
|
float z_base = VERTICES[index][2] / NORMALIZATION * radius_;
|
||||||
|
applyRotations(x_base, y_base, z_base, x, y, z);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Puntos restantes: distribuir en caras usando interpolación
|
||||||
|
// El icosaedro tiene 20 caras triangulares
|
||||||
|
int remaining_points = index - 12;
|
||||||
|
int points_per_face = (num_points_ - 12) / 20;
|
||||||
|
points_per_face = std::max(points_per_face, 1);
|
||||||
|
|
||||||
|
int face_index = remaining_points / points_per_face;
|
||||||
|
if (face_index >= 20) {
|
||||||
|
face_index = 19;
|
||||||
|
}
|
||||||
|
|
||||||
|
int point_in_face = remaining_points % points_per_face;
|
||||||
|
|
||||||
|
// Definir algunas caras del icosaedro (usando índices de vértices)
|
||||||
|
// Solo necesitamos generar puntos, no renderizar caras completas
|
||||||
|
static constexpr std::array<std::array<int, 3>, 20> FACES = {{
|
||||||
|
{0, 11, 5},
|
||||||
|
{0, 5, 1},
|
||||||
|
{0, 1, 7},
|
||||||
|
{0, 7, 10},
|
||||||
|
{0, 10, 11},
|
||||||
|
{1, 5, 9},
|
||||||
|
{5, 11, 4},
|
||||||
|
{11, 10, 2},
|
||||||
|
{10, 7, 6},
|
||||||
|
{7, 1, 8},
|
||||||
|
{3, 9, 4},
|
||||||
|
{3, 4, 2},
|
||||||
|
{3, 2, 6},
|
||||||
|
{3, 6, 8},
|
||||||
|
{3, 8, 9},
|
||||||
|
{4, 9, 5},
|
||||||
|
{2, 4, 11},
|
||||||
|
{6, 2, 10},
|
||||||
|
{8, 6, 7},
|
||||||
|
{9, 8, 1}}};
|
||||||
|
|
||||||
|
// Obtener vértices de la cara
|
||||||
|
int v0 = FACES[face_index][0];
|
||||||
|
int v1 = FACES[face_index][1];
|
||||||
|
int v2 = FACES[face_index][2];
|
||||||
|
|
||||||
|
// Interpolar dentro del triángulo usando coordenadas baricéntricas simples
|
||||||
|
float t = static_cast<float>(point_in_face) / static_cast<float>(points_per_face + 1);
|
||||||
|
float u = sqrtf(t);
|
||||||
|
float v = t - u;
|
||||||
|
|
||||||
|
float x_interp = (VERTICES[v0][0] * (1.0f - u - v)) + (VERTICES[v1][0] * u) + (VERTICES[v2][0] * v);
|
||||||
|
float y_interp = (VERTICES[v0][1] * (1.0f - u - v)) + (VERTICES[v1][1] * u) + (VERTICES[v2][1] * v);
|
||||||
|
float z_interp = (VERTICES[v0][2] * (1.0f - u - v)) + (VERTICES[v1][2] * u) + (VERTICES[v2][2] * v);
|
||||||
|
|
||||||
|
// Proyectar a la esfera
|
||||||
|
float len = sqrtf((x_interp * x_interp) + (y_interp * y_interp) + (z_interp * z_interp));
|
||||||
|
if (len > 0.0001f) {
|
||||||
|
x_interp /= len;
|
||||||
|
y_interp /= len;
|
||||||
|
z_interp /= len;
|
||||||
|
}
|
||||||
|
|
||||||
|
float x_base = x_interp * radius_;
|
||||||
|
float y_base = y_interp * radius_;
|
||||||
|
float z_base = z_interp * radius_;
|
||||||
|
|
||||||
|
applyRotations(x_base, y_base, z_base, x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IcosahedronShape::applyRotations(float x_in, float y_in, float z_in, float& x_out, float& y_out, float& z_out) const {
|
||||||
|
// Aplicar rotación en eje X
|
||||||
|
float cos_x = cosf(angle_x_);
|
||||||
|
float sin_x = sinf(angle_x_);
|
||||||
|
float y_rot_x = (y_in * cos_x) - (z_in * sin_x);
|
||||||
|
float z_rot_x = (y_in * sin_x) + (z_in * cos_x);
|
||||||
|
|
||||||
|
// Aplicar rotación en eje Y
|
||||||
|
float cos_y = cosf(angle_y_);
|
||||||
|
float sin_y = sinf(angle_y_);
|
||||||
|
float x_rot_y = (x_in * cos_y) - (z_rot_x * sin_y);
|
||||||
|
float z_rot_y = (x_in * sin_y) + (z_rot_x * cos_y);
|
||||||
|
|
||||||
|
// Aplicar rotación en eje Z
|
||||||
|
float cos_z = cosf(angle_z_);
|
||||||
|
float sin_z = sinf(angle_z_);
|
||||||
|
float x_final = (x_rot_y * cos_z) - (y_rot_x * sin_z);
|
||||||
|
float y_final = (x_rot_y * sin_z) + (y_rot_x * cos_z);
|
||||||
|
|
||||||
|
x_out = x_final;
|
||||||
|
y_out = y_final;
|
||||||
|
z_out = z_rot_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto IcosahedronShape::getScaleFactor(float screen_height) const -> float {
|
||||||
|
// Factor de escala para física: proporcional al radio
|
||||||
|
// Radio base = 72px (0.30 * 240px en resolución 320x240)
|
||||||
|
const float BASE_RADIUS = 72.0f;
|
||||||
|
float current_radius = screen_height * ICOSAHEDRON_RADIUS_FACTOR;
|
||||||
|
return current_radius / BASE_RADIUS;
|
||||||
|
}
|
||||||
25
source/shapes/icosahedron_shape.hpp
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "shape.hpp"
|
||||||
|
|
||||||
|
// Figura: Icosaedro 3D (D20, poliedro regular de 20 caras)
|
||||||
|
// Comportamiento: 12 vértices distribuidos uniformemente con rotación triple
|
||||||
|
// Geometría: Basado en proporción áurea (golden ratio)
|
||||||
|
class IcosahedronShape : public Shape {
|
||||||
|
private:
|
||||||
|
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
|
||||||
|
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
||||||
|
float angle_z_ = 0.0f; // Ángulo de rotación en eje Z (rad)
|
||||||
|
float radius_ = 0.0f; // Radio de la esfera circunscrita (píxeles)
|
||||||
|
int num_points_ = 0; // Cantidad de puntos generados
|
||||||
|
|
||||||
|
// Helper para aplicar rotaciones triple XYZ
|
||||||
|
void applyRotations(float x_in, float y_in, float z_in, float& x_out, float& y_out, float& z_out) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
const char* getName() const override { return "ICOSAHEDRON"; }
|
||||||
|
float getScaleFactor(float screen_height) const override;
|
||||||
|
};
|
||||||
66
source/shapes/lissajous_shape.cpp
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#include "lissajous_shape.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "defines.hpp"
|
||||||
|
|
||||||
|
void LissajousShape::generatePoints(int num_points, float screen_width, float screen_height) {
|
||||||
|
num_points_ = num_points;
|
||||||
|
amplitude_ = screen_height * LISSAJOUS_SIZE_FACTOR;
|
||||||
|
|
||||||
|
// Inicializar frecuencias desde defines.h
|
||||||
|
freq_x_ = LISSAJOUS_FREQ_X;
|
||||||
|
freq_y_ = LISSAJOUS_FREQ_Y;
|
||||||
|
freq_z_ = LISSAJOUS_FREQ_Z;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LissajousShape::update(float delta_time, float screen_width, float screen_height) {
|
||||||
|
// Recalcular amplitud por si cambió resolución (F4)
|
||||||
|
amplitude_ = screen_height * LISSAJOUS_SIZE_FACTOR;
|
||||||
|
|
||||||
|
// Actualizar rotación global
|
||||||
|
rotation_x_ += LISSAJOUS_ROTATION_SPEED_X * delta_time;
|
||||||
|
rotation_y_ += LISSAJOUS_ROTATION_SPEED_Y * delta_time;
|
||||||
|
|
||||||
|
// Actualizar fase para animación (morphing de la curva)
|
||||||
|
phase_x_ += LISSAJOUS_PHASE_SPEED * delta_time;
|
||||||
|
phase_z_ += LISSAJOUS_PHASE_SPEED * delta_time * 0.7f; // Z rota más lento para variación
|
||||||
|
}
|
||||||
|
|
||||||
|
void LissajousShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||||
|
// Mapear índice [0, num_points-1] a parámetro t [0, 2π]
|
||||||
|
float t = (static_cast<float>(index) / static_cast<float>(num_points_)) * 2.0f * PI;
|
||||||
|
|
||||||
|
// Ecuaciones de Lissajous 3D
|
||||||
|
// x(t) = A * sin(freq_x * t + phase_x)
|
||||||
|
// y(t) = A * sin(freq_y * t)
|
||||||
|
// z(t) = A * sin(freq_z * t + phase_z)
|
||||||
|
float x_local = amplitude_ * sinf((freq_x_ * t) + phase_x_);
|
||||||
|
float y_local = amplitude_ * sinf(freq_y_ * t);
|
||||||
|
float z_local = amplitude_ * sinf((freq_z_ * t) + phase_z_);
|
||||||
|
|
||||||
|
// Aplicar rotación global en eje X
|
||||||
|
float cos_x = cosf(rotation_x_);
|
||||||
|
float sin_x = sinf(rotation_x_);
|
||||||
|
float y_rot = (y_local * cos_x) - (z_local * sin_x);
|
||||||
|
float z_rot = (y_local * sin_x) + (z_local * cos_x);
|
||||||
|
|
||||||
|
// Aplicar rotación global en eje Y
|
||||||
|
float cos_y = cosf(rotation_y_);
|
||||||
|
float sin_y = sinf(rotation_y_);
|
||||||
|
float x_final = (x_local * cos_y) - (z_rot * sin_y);
|
||||||
|
float z_final = (x_local * sin_y) + (z_rot * cos_y);
|
||||||
|
|
||||||
|
// Retornar coordenadas rotadas
|
||||||
|
x = x_final;
|
||||||
|
y = y_rot;
|
||||||
|
z = z_final;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto LissajousShape::getScaleFactor(float screen_height) const -> float {
|
||||||
|
// Factor de escala para física: proporcional a la amplitud de la curva
|
||||||
|
// Amplitud base = 84px (0.35 * 240px en resolución 320x240)
|
||||||
|
const float BASE_SIZE = 84.0f;
|
||||||
|
float current_size = screen_height * LISSAJOUS_SIZE_FACTOR;
|
||||||
|
return current_size / BASE_SIZE;
|
||||||
|
}
|
||||||
26
source/shapes/lissajous_shape.hpp
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "shape.hpp"
|
||||||
|
|
||||||
|
// Figura: Curva de Lissajous 3D
|
||||||
|
// Comportamiento: Curva paramétrica 3D con rotación global y animación de fase
|
||||||
|
// Ecuaciones: x(t) = A*sin(freq_x*t + phase_x), y(t) = A*sin(freq_y*t), z(t) = A*sin(freq_z*t + phase_z)
|
||||||
|
class LissajousShape : public Shape {
|
||||||
|
private:
|
||||||
|
float freq_x_ = 0.0f; // Frecuencia en eje X
|
||||||
|
float freq_y_ = 0.0f; // Frecuencia en eje Y
|
||||||
|
float freq_z_ = 0.0f; // Frecuencia en eje Z
|
||||||
|
float phase_x_ = 0.0f; // Desfase X (animado)
|
||||||
|
float phase_z_ = 0.0f; // Desfase Z (animado)
|
||||||
|
float rotation_x_ = 0.0f; // Rotación global en eje X (rad)
|
||||||
|
float rotation_y_ = 0.0f; // Rotación global en eje Y (rad)
|
||||||
|
float amplitude_ = 0.0f; // Amplitud de la curva (píxeles)
|
||||||
|
int num_points_ = 0; // Cantidad total de puntos
|
||||||
|
|
||||||
|
public:
|
||||||
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
const char* getName() const override { return "LISSAJOUS"; }
|
||||||
|
float getScaleFactor(float screen_height) const override;
|
||||||
|
};
|
||||||
445
source/shapes/png_shape.cpp
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
#include "png_shape.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <iostream>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
#include "defines.hpp"
|
||||||
|
#include "external/stb_image.h"
|
||||||
|
#include "resource_manager.hpp"
|
||||||
|
|
||||||
|
PNGShape::PNGShape(const char* png_path) {
|
||||||
|
// Cargar PNG desde path
|
||||||
|
if (!loadPNG(png_path)) {
|
||||||
|
std::cerr << "[PNGShape] Usando fallback 10x10" << '\n';
|
||||||
|
// Fallback: generar un cuadrado simple si falla la carga
|
||||||
|
image_width_ = 10;
|
||||||
|
image_height_ = 10;
|
||||||
|
pixel_data_.resize(100, true); // Cuadrado 10x10 blanco
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializar next_idle_time_ con valores apropiados (no hardcoded 5.0)
|
||||||
|
next_idle_time_ = PNG_IDLE_TIME_MIN + (rand() % 1000) / 1000.0f * (PNG_IDLE_TIME_MAX - PNG_IDLE_TIME_MIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto PNGShape::loadPNG(const char* resource_key) -> bool {
|
||||||
|
{
|
||||||
|
std::string fn = std::string(resource_key);
|
||||||
|
fn = fn.substr(fn.find_last_of("\\/") + 1);
|
||||||
|
std::cout << "[PNGShape] " << fn << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
|
||||||
|
}
|
||||||
|
unsigned char* file_data = nullptr;
|
||||||
|
size_t file_size = 0;
|
||||||
|
if (!ResourceManager::loadResource(resource_key, file_data, file_size)) {
|
||||||
|
std::cerr << "[PNGShape] ERROR: recurso no encontrado: " << resource_key << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
int channels;
|
||||||
|
unsigned char* pixels = stbi_load_from_memory(file_data, static_cast<int>(file_size), &width, &height, &channels, 1);
|
||||||
|
delete[] file_data;
|
||||||
|
if (pixels == nullptr) {
|
||||||
|
std::cerr << "[PNGShape] ERROR al decodificar PNG: " << stbi_failure_reason() << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
image_width_ = width;
|
||||||
|
image_height_ = height;
|
||||||
|
pixel_data_.resize(width * height);
|
||||||
|
for (int i = 0; i < width * height; i++) {
|
||||||
|
pixel_data_[i] = (pixels[i] > 128);
|
||||||
|
}
|
||||||
|
stbi_image_free(pixels);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PNGShape::detectEdges() {
|
||||||
|
edge_points_.clear();
|
||||||
|
|
||||||
|
// Detectar píxeles del contorno (píxeles blancos con al menos un vecino negro)
|
||||||
|
for (int y = 0; y < image_height_; y++) {
|
||||||
|
for (int x = 0; x < image_width_; x++) {
|
||||||
|
int idx = (y * image_width_) + x;
|
||||||
|
|
||||||
|
if (!pixel_data_[idx]) {
|
||||||
|
continue; // Solo píxeles blancos
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar vecinos (arriba, abajo, izq, der)
|
||||||
|
bool is_edge = false;
|
||||||
|
|
||||||
|
if (x == 0 || x == image_width_ - 1 || y == 0 || y == image_height_ - 1) {
|
||||||
|
is_edge = true; // Bordes de la imagen
|
||||||
|
} else {
|
||||||
|
// Verificar 4 vecinos
|
||||||
|
if (!pixel_data_[idx - 1] || // Izquierda
|
||||||
|
!pixel_data_[idx + 1] || // Derecha
|
||||||
|
!pixel_data_[idx - image_width_] || // Arriba
|
||||||
|
!pixel_data_[idx + image_width_]) { // Abajo
|
||||||
|
is_edge = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_edge) {
|
||||||
|
edge_points_.push_back({static_cast<float>(x), static_cast<float>(y)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PNGShape::floodFill() {
|
||||||
|
// TODO: Implementar flood-fill para Enfoque B (voxelización)
|
||||||
|
// Por ahora, rellenar con todos los píxeles blancos
|
||||||
|
filled_points_.clear();
|
||||||
|
|
||||||
|
for (int y = 0; y < image_height_; y++) {
|
||||||
|
for (int x = 0; x < image_width_; x++) {
|
||||||
|
int idx = (y * image_width_) + x;
|
||||||
|
if (pixel_data_[idx]) {
|
||||||
|
filled_points_.push_back({static_cast<float>(x), static_cast<float>(y)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PNGShape::generateExtrudedPoints() {
|
||||||
|
if (PNG_USE_EDGES_ONLY) {
|
||||||
|
// Usar solo bordes (contorno) de las letras
|
||||||
|
detectEdges();
|
||||||
|
} else {
|
||||||
|
// Usar relleno completo (todos los píxeles blancos)
|
||||||
|
floodFill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PNGShape::generatePoints(int num_points, float screen_width, float screen_height) {
|
||||||
|
num_points_ = num_points;
|
||||||
|
extrusion_depth_ = screen_height * PNG_EXTRUSION_DEPTH_FACTOR;
|
||||||
|
num_layers_ = PNG_NUM_EXTRUSION_LAYERS;
|
||||||
|
|
||||||
|
// Generar AMBOS conjuntos de puntos (relleno Y bordes)
|
||||||
|
floodFill(); // Generar filled_points_
|
||||||
|
detectEdges(); // Generar edge_points_
|
||||||
|
|
||||||
|
// Guardar copias originales (las funciones de filtrado modifican los vectores)
|
||||||
|
std::vector<Point2D> filled_points_original = filled_points_;
|
||||||
|
std::vector<Point2D> edge_points_original = edge_points_;
|
||||||
|
|
||||||
|
// Conjunto de puntos ACTIVO (será modificado por filtros)
|
||||||
|
std::vector<Point2D> active_points_data;
|
||||||
|
std::string mode_name;
|
||||||
|
|
||||||
|
// === SISTEMA DE DISTRIBUCIÓN ADAPTATIVA ===
|
||||||
|
// Estrategia: Optimizar según número de pelotas disponibles
|
||||||
|
// Objetivo: SIEMPRE intentar usar relleno primero, solo bordes si es necesario
|
||||||
|
|
||||||
|
size_t num_2d_points = 0;
|
||||||
|
size_t total_3d_points = 0;
|
||||||
|
|
||||||
|
// NIVEL 1: Decidir punto de partida (relleno o bordes por configuración)
|
||||||
|
if (PNG_USE_EDGES_ONLY) {
|
||||||
|
active_points_data = edge_points_original;
|
||||||
|
mode_name = "BORDES (config)";
|
||||||
|
} else {
|
||||||
|
active_points_data = filled_points_original;
|
||||||
|
mode_name = "RELLENO";
|
||||||
|
}
|
||||||
|
|
||||||
|
num_2d_points = active_points_data.size();
|
||||||
|
total_3d_points = num_2d_points * num_layers_;
|
||||||
|
|
||||||
|
// NIVEL 2: Reducir capas AGRESIVAMENTE hasta 1 (priorizar calidad 2D sobre profundidad 3D)
|
||||||
|
// Objetivo: Llenar bien el texto en 2D antes de reducir píxeles
|
||||||
|
while (num_layers_ > 1 && num_points < static_cast<int>(total_3d_points)) {
|
||||||
|
num_layers_ = std::max(1, num_layers_ / 2);
|
||||||
|
total_3d_points = num_2d_points * num_layers_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NIVEL 3: Filas alternas en RELLENO (solo si 1 capa no alcanza)
|
||||||
|
// Esto permite usar relleno incluso con pocas pelotas
|
||||||
|
int row_skip = 1;
|
||||||
|
if (!PNG_USE_EDGES_ONLY) { // Solo si empezamos con relleno
|
||||||
|
while (row_skip < 5 && num_points < static_cast<int>(total_3d_points)) {
|
||||||
|
row_skip++;
|
||||||
|
// ✅ CLAVE: Recalcular desde el ORIGINAL cada vez (no desde el filtrado previo)
|
||||||
|
active_points_data = extractAlternateRows(filled_points_original, row_skip);
|
||||||
|
num_2d_points = active_points_data.size();
|
||||||
|
total_3d_points = num_2d_points * num_layers_;
|
||||||
|
mode_name = "RELLENO + FILAS/" + std::to_string(row_skip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NIVEL 4: Cambiar a BORDES (solo si relleno con filas alternas no alcanza)
|
||||||
|
if (!PNG_USE_EDGES_ONLY && num_points < static_cast<int>(total_3d_points)) {
|
||||||
|
active_points_data = edge_points_original;
|
||||||
|
mode_name = "BORDES (auto)";
|
||||||
|
num_2d_points = active_points_data.size();
|
||||||
|
total_3d_points = num_2d_points * num_layers_;
|
||||||
|
row_skip = 1; // Reset row_skip para bordes
|
||||||
|
}
|
||||||
|
|
||||||
|
// NIVEL 5: Filas alternas en BORDES (si aún no alcanza)
|
||||||
|
while (row_skip < 8 && num_points < static_cast<int>(total_3d_points)) {
|
||||||
|
row_skip++;
|
||||||
|
// ✅ CLAVE: Recalcular desde edge_points_original cada vez
|
||||||
|
active_points_data = extractAlternateRows(edge_points_original, row_skip);
|
||||||
|
num_2d_points = active_points_data.size();
|
||||||
|
total_3d_points = num_2d_points * num_layers_;
|
||||||
|
if (mode_name.find("FILAS") == std::string::npos) {
|
||||||
|
mode_name += " + FILAS/" + std::to_string(row_skip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NIVEL 6: Vértices/esquinas (último recurso, muy pocas pelotas)
|
||||||
|
if (num_points < static_cast<int>(total_3d_points) && num_points < 150) {
|
||||||
|
// Determinar desde qué conjunto extraer vértices (el que esté activo actualmente)
|
||||||
|
const std::vector<Point2D>& source_for_vertices = (mode_name.find("BORDES") != std::string::npos)
|
||||||
|
? edge_points_original
|
||||||
|
: filled_points_original;
|
||||||
|
|
||||||
|
std::vector<Point2D> vertices = extractCornerVertices(source_for_vertices);
|
||||||
|
if (!vertices.empty() && vertices.size() < active_points_data.size()) {
|
||||||
|
active_points_data = vertices;
|
||||||
|
mode_name = "VÉRTICES";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ CLAVE: Guardar el conjunto de puntos optimizado final en optimized_points_ (usado por getPoint3D)
|
||||||
|
optimized_points_ = active_points_data;
|
||||||
|
|
||||||
|
// Calcular escala para centrar la imagen en pantalla
|
||||||
|
float max_dimension = std::max(static_cast<float>(image_width_), static_cast<float>(image_height_));
|
||||||
|
scale_factor_ = (screen_height * PNG_SIZE_FACTOR) / max_dimension;
|
||||||
|
|
||||||
|
// Calcular offset para centrar
|
||||||
|
center_offset_x_ = image_width_ * 0.5f;
|
||||||
|
center_offset_y_ = image_height_ * 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extraer filas alternas de puntos (FUNCIÓN PURA: no modifica parámetros)
|
||||||
|
// Recibe vector original y devuelve nuevo vector filtrado
|
||||||
|
auto PNGShape::extractAlternateRows(const std::vector<Point2D>& source, int row_skip) -> std::vector<PNGShape::Point2D> {
|
||||||
|
std::vector<Point2D> result;
|
||||||
|
|
||||||
|
if (row_skip <= 1 || source.empty()) {
|
||||||
|
return source; // Sin filtrado, devolver copia del original
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organizar puntos por fila (Y)
|
||||||
|
std::map<int, std::vector<Point2D>> rows;
|
||||||
|
for (const auto& p : source) {
|
||||||
|
int row = static_cast<int>(p.y);
|
||||||
|
rows[row].push_back(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tomar solo cada N filas
|
||||||
|
int row_counter = 0;
|
||||||
|
for (const auto& [row_y, row_points] : rows) {
|
||||||
|
if (row_counter % row_skip == 0) {
|
||||||
|
result.insert(result.end(), row_points.begin(), row_points.end());
|
||||||
|
}
|
||||||
|
row_counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extraer vértices y esquinas (FUNCIÓN PURA: devuelve nuevo vector)
|
||||||
|
auto PNGShape::extractCornerVertices(const std::vector<Point2D>& source) -> std::vector<PNGShape::Point2D> {
|
||||||
|
std::vector<Point2D> result;
|
||||||
|
|
||||||
|
if (source.empty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estrategia simple: tomar bordes extremos de cada fila
|
||||||
|
// Esto da el "esqueleto" mínimo de las letras
|
||||||
|
|
||||||
|
std::map<int, std::pair<float, float>> row_extremes; // Y -> (min_x, max_x)
|
||||||
|
|
||||||
|
for (const auto& p : source) {
|
||||||
|
int row = static_cast<int>(p.y);
|
||||||
|
if (row_extremes.find(row) == row_extremes.end()) {
|
||||||
|
row_extremes[row] = {p.x, p.x};
|
||||||
|
} else {
|
||||||
|
row_extremes[row].first = std::min(row_extremes[row].first, p.x);
|
||||||
|
row_extremes[row].second = std::max(row_extremes[row].second, p.x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generar puntos en extremos de cada fila
|
||||||
|
for (const auto& [row_y, extremes] : row_extremes) {
|
||||||
|
result.push_back({extremes.first, static_cast<float>(row_y)}); // Extremo izquierdo
|
||||||
|
if (extremes.second != extremes.first) { // Solo añadir derecho si es diferente
|
||||||
|
result.push_back({extremes.second, static_cast<float>(row_y)}); // Extremo derecho
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PNGShape::update(float delta_time, float screen_width, float screen_height) {
|
||||||
|
if (!is_flipping_) {
|
||||||
|
// Estado IDLE: texto de frente con pivoteo sutil
|
||||||
|
|
||||||
|
// Solo contar tiempo para flips si:
|
||||||
|
// - NO está en modo LOGO, O
|
||||||
|
// - Está en modo LOGO Y ha alcanzado umbral de convergencia (80%)
|
||||||
|
bool can_start_flip = !is_logo_mode_ || convergence_threshold_reached_;
|
||||||
|
|
||||||
|
if (can_start_flip) {
|
||||||
|
idle_timer_ += delta_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pivoteo sutil constante (movimiento orgánico)
|
||||||
|
tilt_x_ += 0.4f * delta_time; // Velocidad sutil en X
|
||||||
|
tilt_y_ += 0.6f * delta_time; // Velocidad sutil en Y
|
||||||
|
|
||||||
|
if (idle_timer_ >= next_idle_time_) {
|
||||||
|
// Iniciar voltereta
|
||||||
|
is_flipping_ = true;
|
||||||
|
flip_timer_ = 0.0f;
|
||||||
|
idle_timer_ = 0.0f;
|
||||||
|
|
||||||
|
// Elegir eje aleatorio (0=X, 1=Y, 2=ambos)
|
||||||
|
flip_axis_ = rand() % 3;
|
||||||
|
|
||||||
|
// Próximo tiempo idle aleatorio (según modo LOGO o MANUAL)
|
||||||
|
float idle_min = is_logo_mode_ ? PNG_IDLE_TIME_MIN_LOGO : PNG_IDLE_TIME_MIN;
|
||||||
|
float idle_max = is_logo_mode_ ? PNG_IDLE_TIME_MAX_LOGO : PNG_IDLE_TIME_MAX;
|
||||||
|
next_idle_time_ = idle_min + (rand() % 1000) / 1000.0f * (idle_max - idle_min);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Estado FLIP: voltereta en curso
|
||||||
|
flip_timer_ += delta_time;
|
||||||
|
|
||||||
|
// Rotar según eje elegido
|
||||||
|
if (flip_axis_ == 0 || flip_axis_ == 2) {
|
||||||
|
angle_x_ += PNG_FLIP_SPEED * delta_time;
|
||||||
|
}
|
||||||
|
if (flip_axis_ == 1 || flip_axis_ == 2) {
|
||||||
|
angle_y_ += PNG_FLIP_SPEED * delta_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminar voltereta
|
||||||
|
if (flip_timer_ >= PNG_FLIP_DURATION) {
|
||||||
|
is_flipping_ = false;
|
||||||
|
// Resetear ángulos a 0 (volver de frente)
|
||||||
|
angle_x_ = 0.0f;
|
||||||
|
angle_y_ = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detectar transición de flip (de true a false) para incrementar contador
|
||||||
|
if (was_flipping_last_frame_ && !is_flipping_) {
|
||||||
|
flip_count_++; // Flip completado
|
||||||
|
}
|
||||||
|
was_flipping_last_frame_ = is_flipping_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||||
|
// Usar SIEMPRE el vector optimizado (resultado final de generatePoints)
|
||||||
|
const std::vector<Point2D>& points = optimized_points_;
|
||||||
|
|
||||||
|
if (points.empty()) {
|
||||||
|
x = y = z = 0.0f;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENFOQUE A: Extrusión 2D
|
||||||
|
// Cada punto 2D se replica en múltiples capas Z
|
||||||
|
|
||||||
|
int num_2d_points = static_cast<int>(points.size());
|
||||||
|
int point_2d_index = index % num_2d_points;
|
||||||
|
int layer_index = (index / num_2d_points) % num_layers_;
|
||||||
|
|
||||||
|
// Obtener coordenadas 2D del píxel
|
||||||
|
Point2D p = points[point_2d_index];
|
||||||
|
|
||||||
|
// Centrar y escalar
|
||||||
|
float x_base = (p.x - center_offset_x_) * scale_factor_;
|
||||||
|
float y_base = (p.y - center_offset_y_) * scale_factor_;
|
||||||
|
|
||||||
|
// Calcular Z según capa (distribuir uniformemente en profundidad)
|
||||||
|
float z_base = 0.0f;
|
||||||
|
if (num_layers_ > 1) {
|
||||||
|
float layer_step = extrusion_depth_ / static_cast<float>(num_layers_ - 1);
|
||||||
|
z_base = -extrusion_depth_ * 0.5f + layer_index * layer_step;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Añadir pivoteo sutil en estado IDLE
|
||||||
|
// Calcular tamaño del logo en pantalla para normalizar correctamente
|
||||||
|
float logo_width = image_width_ * scale_factor_;
|
||||||
|
float logo_height = image_height_ * scale_factor_;
|
||||||
|
float logo_size = std::max(logo_width, logo_height);
|
||||||
|
|
||||||
|
// Normalizar coordenadas a rango [-1, 1]
|
||||||
|
float u = x_base / (logo_size * 0.5f);
|
||||||
|
float v = y_base / (logo_size * 0.5f);
|
||||||
|
|
||||||
|
// Calcular pivoteo (amplitudes más grandes)
|
||||||
|
float tilt_amount_x = sinf(tilt_x_) * 0.15f; // 15%
|
||||||
|
float tilt_amount_y = sinf(tilt_y_) * 0.1f; // 10%
|
||||||
|
|
||||||
|
// Aplicar pivoteo proporcional al tamaño del logo
|
||||||
|
float z_tilt = (u * tilt_amount_y + v * tilt_amount_x) * logo_size;
|
||||||
|
z_base += z_tilt; // Añadir pivoteo sutil a la profundidad
|
||||||
|
|
||||||
|
// Aplicar rotación en eje Y (horizontal)
|
||||||
|
float cos_y = cosf(angle_y_);
|
||||||
|
float sin_y = sinf(angle_y_);
|
||||||
|
float x_rot_y = (x_base * cos_y) - (z_base * sin_y);
|
||||||
|
float z_rot_y = (x_base * sin_y) + (z_base * cos_y);
|
||||||
|
|
||||||
|
// Aplicar rotación en eje X (vertical)
|
||||||
|
float cos_x = cosf(angle_x_);
|
||||||
|
float sin_x = sinf(angle_x_);
|
||||||
|
float y_rot = (y_base * cos_x) - (z_rot_y * sin_x);
|
||||||
|
float z_rot = (y_base * sin_x) + (z_rot_y * cos_x);
|
||||||
|
|
||||||
|
// Retornar coordenadas finales
|
||||||
|
x = x_rot_y;
|
||||||
|
y = y_rot;
|
||||||
|
|
||||||
|
// Cuando está de frente (sin rotación), usar Z con pivoteo sutil
|
||||||
|
if (angle_x_ == 0.0f && angle_y_ == 0.0f) {
|
||||||
|
// De frente: usar z_base que incluye el pivoteo sutil
|
||||||
|
z = z_base;
|
||||||
|
} else {
|
||||||
|
z = z_rot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto PNGShape::getScaleFactor(float screen_height) const -> float {
|
||||||
|
// Escala dinámica según resolución
|
||||||
|
return PNG_SIZE_FACTOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sistema de convergencia: notificar a la figura sobre el % de pelotas en posición
|
||||||
|
void PNGShape::setConvergence(float convergence) {
|
||||||
|
current_convergence_ = convergence;
|
||||||
|
|
||||||
|
// Umbral de convergencia
|
||||||
|
constexpr float CONVERGENCE_THRESHOLD = 0.4f;
|
||||||
|
|
||||||
|
// Activar threshold cuando convergencia supera el umbral
|
||||||
|
if (!convergence_threshold_reached_ && convergence >= CONVERGENCE_THRESHOLD) {
|
||||||
|
convergence_threshold_reached_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desactivar threshold cuando convergencia cae por debajo del umbral
|
||||||
|
if (convergence < CONVERGENCE_THRESHOLD) {
|
||||||
|
convergence_threshold_reached_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener progreso del flip actual (0.0 = inicio del flip, 1.0 = fin del flip)
|
||||||
|
auto PNGShape::getFlipProgress() const -> float {
|
||||||
|
if (!is_flipping_) {
|
||||||
|
return 0.0f; // No está flipping, progreso = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular progreso normalizado (0.0 - 1.0)
|
||||||
|
return flip_timer_ / PNG_FLIP_DURATION;
|
||||||
|
}
|
||||||
108
source/shapes/png_shape.hpp
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdlib> // Para rand()
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "defines.hpp" // Para PNG_IDLE_TIME_MIN/MAX constantes
|
||||||
|
#include "shape.hpp"
|
||||||
|
|
||||||
|
// Figura: Shape generada desde PNG 1-bit (blanco sobre negro)
|
||||||
|
// Enfoque A: Extrusión 2D (implementado)
|
||||||
|
// Enfoque B: Voxelización 3D (preparado para futuro)
|
||||||
|
class PNGShape : public Shape {
|
||||||
|
private:
|
||||||
|
// Datos de la imagen cargada
|
||||||
|
int image_width_ = 0;
|
||||||
|
int image_height_ = 0;
|
||||||
|
std::vector<bool> pixel_data_; // Mapa de píxeles blancos (true = blanco)
|
||||||
|
|
||||||
|
// Puntos generados (Enfoque A: Extrusión 2D)
|
||||||
|
struct Point2D {
|
||||||
|
float x, y;
|
||||||
|
};
|
||||||
|
std::vector<Point2D> edge_points_; // Contorno (solo bordes) - ORIGINAL sin optimizar
|
||||||
|
std::vector<Point2D> filled_points_; // Relleno completo - ORIGINAL sin optimizar
|
||||||
|
std::vector<Point2D> optimized_points_; // Puntos finales optimizados (usado por getPoint3D)
|
||||||
|
|
||||||
|
// Parámetros de extrusión
|
||||||
|
float extrusion_depth_ = 0.0f; // Profundidad de extrusión en Z
|
||||||
|
int num_layers_ = 0; // Capas de extrusión (más capas = más denso)
|
||||||
|
|
||||||
|
// Rotación "legible" (de frente con volteretas ocasionales)
|
||||||
|
float angle_x_ = 0.0f;
|
||||||
|
float angle_y_ = 0.0f;
|
||||||
|
float idle_timer_ = 0.0f; // Timer para tiempo de frente
|
||||||
|
float flip_timer_ = 0.0f; // Timer para voltereta
|
||||||
|
float next_idle_time_ = 5.0f; // Próximo tiempo de espera (aleatorio)
|
||||||
|
bool is_flipping_ = false; // Estado: quieto o voltereta
|
||||||
|
int flip_axis_ = 0; // Eje de voltereta (0=X, 1=Y, 2=ambos)
|
||||||
|
|
||||||
|
// Pivoteo sutil en estado IDLE
|
||||||
|
float tilt_x_ = 0.0f; // Oscilación sutil en eje X
|
||||||
|
float tilt_y_ = 0.0f; // Oscilación sutil en eje Y
|
||||||
|
|
||||||
|
// Modo LOGO (intervalos de flip más largos)
|
||||||
|
bool is_logo_mode_ = false; // true = usar intervalos LOGO (más lentos)
|
||||||
|
|
||||||
|
// Sistema de convergencia (solo relevante en modo LOGO)
|
||||||
|
float current_convergence_ = 0.0f; // Porcentaje actual de convergencia (0.0-1.0)
|
||||||
|
bool convergence_threshold_reached_ = false; // true si ha alcanzado umbral mínimo (80%)
|
||||||
|
|
||||||
|
// Sistema de tracking de flips (para modo LOGO - espera de flips)
|
||||||
|
int flip_count_ = 0; // Contador de flips completados (reset al entrar a LOGO)
|
||||||
|
bool was_flipping_last_frame_ = false; // Estado previo para detectar transiciones
|
||||||
|
|
||||||
|
// Dimensiones normalizadas
|
||||||
|
float scale_factor_ = 1.0f;
|
||||||
|
float center_offset_x_ = 0.0f;
|
||||||
|
float center_offset_y_ = 0.0f;
|
||||||
|
|
||||||
|
int num_points_ = 0; // Total de puntos generados (para indexación)
|
||||||
|
|
||||||
|
// Métodos internos
|
||||||
|
bool loadPNG(const char* resource_key); // Cargar PNG con stb_image
|
||||||
|
void detectEdges(); // Detectar contorno (Enfoque A)
|
||||||
|
void floodFill(); // Rellenar interior (Enfoque B - futuro)
|
||||||
|
void generateExtrudedPoints(); // Generar puntos con extrusión 2D
|
||||||
|
|
||||||
|
// Métodos de distribución adaptativa (funciones puras, no modifican parámetros)
|
||||||
|
static std::vector<Point2D> extractAlternateRows(const std::vector<Point2D>& source, int row_skip); // Extraer filas alternas
|
||||||
|
static std::vector<Point2D> extractCornerVertices(const std::vector<Point2D>& source); // Extraer vértices/esquinas
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Constructor: recibe path relativo al PNG
|
||||||
|
PNGShape(const char* png_path = "data/shapes/jailgames.png");
|
||||||
|
|
||||||
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
const char* getName() const override { return "PNG SHAPE"; }
|
||||||
|
float getScaleFactor(float screen_height) const override;
|
||||||
|
|
||||||
|
// Consultar estado de flip
|
||||||
|
bool isFlipping() const { return is_flipping_; }
|
||||||
|
|
||||||
|
// Obtener progreso del flip actual (0.0 = inicio, 1.0 = fin)
|
||||||
|
float getFlipProgress() const;
|
||||||
|
|
||||||
|
// Obtener número de flips completados (para modo LOGO)
|
||||||
|
int getFlipCount() const { return flip_count_; }
|
||||||
|
|
||||||
|
// Resetear contador de flips (llamar al entrar a LOGO MODE)
|
||||||
|
void resetFlipCount() {
|
||||||
|
flip_count_ = 0;
|
||||||
|
was_flipping_last_frame_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control de modo LOGO (flip intervals más largos)
|
||||||
|
void setLogoMode(bool enable) {
|
||||||
|
is_logo_mode_ = enable;
|
||||||
|
// Recalcular next_idle_time_ con el rango apropiado
|
||||||
|
float idle_min = enable ? PNG_IDLE_TIME_MIN_LOGO : PNG_IDLE_TIME_MIN;
|
||||||
|
float idle_max = enable ? PNG_IDLE_TIME_MAX_LOGO : PNG_IDLE_TIME_MAX;
|
||||||
|
next_idle_time_ = idle_min + (rand() % 1000) / 1000.0f * (idle_max - idle_min);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sistema de convergencia (override de Shape::setConvergence)
|
||||||
|
void setConvergence(float convergence) override;
|
||||||
|
};
|
||||||
35
source/shapes/shape.hpp
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Interfaz abstracta para todas las figuras 3D
|
||||||
|
class Shape {
|
||||||
|
public:
|
||||||
|
virtual ~Shape() = default;
|
||||||
|
|
||||||
|
// Generar distribución inicial de puntos en la figura
|
||||||
|
// num_points: cantidad de pelotas a distribuir
|
||||||
|
// screen_width/height: dimensiones del área de juego (para escalar)
|
||||||
|
virtual void generatePoints(int num_points, float screen_width, float screen_height) = 0;
|
||||||
|
|
||||||
|
// Actualizar animación de la figura (rotación, deformación, etc.)
|
||||||
|
// delta_time: tiempo transcurrido desde último frame
|
||||||
|
// screen_width/height: dimensiones actuales (puede cambiar con F4)
|
||||||
|
virtual void update(float delta_time, float screen_width, float screen_height) = 0;
|
||||||
|
|
||||||
|
// Obtener posición 3D del punto i después de transformaciones (rotación, etc.)
|
||||||
|
// index: índice del punto (0 a num_points-1)
|
||||||
|
// x, y, z: coordenadas 3D en espacio mundo (centradas en 0,0,0)
|
||||||
|
virtual void getPoint3D(int index, float& x, float& y, float& z) const = 0;
|
||||||
|
|
||||||
|
// Obtener nombre de la figura para debug display
|
||||||
|
virtual const char* getName() const = 0;
|
||||||
|
|
||||||
|
// Obtener factor de escala para ajustar física según tamaño de figura
|
||||||
|
// screen_height: altura actual de pantalla
|
||||||
|
// Retorna: factor multiplicador para constantes de física (spring_k, damping, etc.)
|
||||||
|
virtual float getScaleFactor(float screen_height) const = 0;
|
||||||
|
|
||||||
|
// Notificar a la figura sobre el porcentaje de convergencia (pelotas cerca del objetivo)
|
||||||
|
// convergence: valor de 0.0 (0%) a 1.0 (100%) indicando cuántas pelotas están en posición
|
||||||
|
// Default: no-op (la mayoría de figuras no necesitan esta información)
|
||||||
|
virtual void setConvergence(float convergence) {}
|
||||||
|
};
|
||||||
60
source/shapes/sphere_shape.cpp
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#include "sphere_shape.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "defines.hpp"
|
||||||
|
|
||||||
|
void SphereShape::generatePoints(int num_points, float screen_width, float screen_height) {
|
||||||
|
num_points_ = num_points;
|
||||||
|
radius_ = screen_height * ROTOBALL_RADIUS_FACTOR;
|
||||||
|
// Las posiciones 3D se calculan en getPoint3D() usando Fibonacci Sphere
|
||||||
|
}
|
||||||
|
|
||||||
|
void SphereShape::update(float delta_time, float screen_width, float screen_height) {
|
||||||
|
// Recalcular radio por si cambió resolución (F4)
|
||||||
|
radius_ = screen_height * ROTOBALL_RADIUS_FACTOR;
|
||||||
|
|
||||||
|
// Actualizar ángulos de rotación
|
||||||
|
angle_y_ += ROTOBALL_ROTATION_SPEED_Y * delta_time;
|
||||||
|
angle_x_ += ROTOBALL_ROTATION_SPEED_X * delta_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SphereShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||||
|
// Algoritmo Fibonacci Sphere para distribución uniforme
|
||||||
|
const float GOLDEN_RATIO = (1.0f + sqrtf(5.0f)) / 2.0f;
|
||||||
|
const float ANGLE_INCREMENT = PI * 2.0f * GOLDEN_RATIO;
|
||||||
|
|
||||||
|
float t = static_cast<float>(index) / static_cast<float>(num_points_);
|
||||||
|
float phi = acosf(1.0f - (2.0f * t)); // Latitud
|
||||||
|
float theta = ANGLE_INCREMENT * static_cast<float>(index); // Longitud
|
||||||
|
|
||||||
|
// Convertir coordenadas esféricas a cartesianas
|
||||||
|
float x_base = cosf(theta) * sinf(phi) * radius_;
|
||||||
|
float y_base = sinf(theta) * sinf(phi) * radius_;
|
||||||
|
float z_base = cosf(phi) * radius_;
|
||||||
|
|
||||||
|
// Aplicar rotación en eje Y
|
||||||
|
float cos_y = cosf(angle_y_);
|
||||||
|
float sin_y = sinf(angle_y_);
|
||||||
|
float x_rot = (x_base * cos_y) - (z_base * sin_y);
|
||||||
|
float z_rot = (x_base * sin_y) + (z_base * cos_y);
|
||||||
|
|
||||||
|
// Aplicar rotación en eje X
|
||||||
|
float cos_x = cosf(angle_x_);
|
||||||
|
float sin_x = sinf(angle_x_);
|
||||||
|
float y_rot = (y_base * cos_x) - (z_rot * sin_x);
|
||||||
|
float z_final = (y_base * sin_x) + (z_rot * cos_x);
|
||||||
|
|
||||||
|
// Retornar coordenadas finales rotadas
|
||||||
|
x = x_rot;
|
||||||
|
y = y_rot;
|
||||||
|
z = z_final;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SphereShape::getScaleFactor(float screen_height) const -> float {
|
||||||
|
// Factor de escala para física: proporcional al radio
|
||||||
|
// Radio base = 80px (resolución 320x240)
|
||||||
|
const float BASE_RADIUS = 80.0f;
|
||||||
|
float current_radius = screen_height * ROTOBALL_RADIUS_FACTOR;
|
||||||
|
return current_radius / BASE_RADIUS;
|
||||||
|
}
|
||||||
21
source/shapes/sphere_shape.hpp
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "shape.hpp"
|
||||||
|
|
||||||
|
// Figura: Esfera 3D con distribución uniforme (Fibonacci Sphere Algorithm)
|
||||||
|
// Comportamiento: Rotación dual en ejes X e Y
|
||||||
|
// Uso anterior: RotoBall
|
||||||
|
class SphereShape : public Shape {
|
||||||
|
private:
|
||||||
|
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
|
||||||
|
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
||||||
|
float radius_ = 0.0f; // Radio de la esfera (píxeles)
|
||||||
|
int num_points_ = 0; // Cantidad de puntos generados
|
||||||
|
|
||||||
|
public:
|
||||||
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
const char* getName() const override { return "SPHERE"; }
|
||||||
|
float getScaleFactor(float screen_height) const override;
|
||||||
|
};
|
||||||
99
source/shapes/torus_shape.cpp
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
#include "torus_shape.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "defines.hpp"
|
||||||
|
|
||||||
|
void TorusShape::generatePoints(int num_points, float screen_width, float screen_height) {
|
||||||
|
num_points_ = num_points;
|
||||||
|
major_radius_ = screen_height * TORUS_MAJOR_RADIUS_FACTOR;
|
||||||
|
minor_radius_ = screen_height * TORUS_MINOR_RADIUS_FACTOR;
|
||||||
|
// Las posiciones 3D se calculan en getPoint3D() usando ecuaciones paramétricas del torus
|
||||||
|
}
|
||||||
|
|
||||||
|
void TorusShape::update(float delta_time, float screen_width, float screen_height) {
|
||||||
|
// Recalcular radios por si cambió resolución (F4)
|
||||||
|
major_radius_ = screen_height * TORUS_MAJOR_RADIUS_FACTOR;
|
||||||
|
minor_radius_ = screen_height * TORUS_MINOR_RADIUS_FACTOR;
|
||||||
|
|
||||||
|
// Actualizar ángulos de rotación (triple rotación XYZ)
|
||||||
|
angle_x_ += TORUS_ROTATION_SPEED_X * delta_time;
|
||||||
|
angle_y_ += TORUS_ROTATION_SPEED_Y * delta_time;
|
||||||
|
angle_z_ += TORUS_ROTATION_SPEED_Z * delta_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TorusShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||||
|
// Distribuir puntos uniformemente en la superficie del torus
|
||||||
|
// Usamos distribución aproximadamente uniforme basada en área
|
||||||
|
|
||||||
|
// Calcular número aproximado de anillos y puntos por anillo
|
||||||
|
int num_rings = static_cast<int>(sqrtf(static_cast<float>(num_points_) * 0.5f));
|
||||||
|
num_rings = std::max(num_rings, 2);
|
||||||
|
|
||||||
|
int points_per_ring = num_points_ / num_rings;
|
||||||
|
points_per_ring = std::max(points_per_ring, 3);
|
||||||
|
|
||||||
|
// Obtener parámetros u y v del índice
|
||||||
|
int ring = index / points_per_ring;
|
||||||
|
int point_in_ring = index % points_per_ring;
|
||||||
|
|
||||||
|
// Si nos pasamos del número de anillos, usar el último
|
||||||
|
if (ring >= num_rings) {
|
||||||
|
ring = num_rings - 1;
|
||||||
|
point_in_ring = index - (ring * points_per_ring);
|
||||||
|
if (point_in_ring >= points_per_ring) {
|
||||||
|
point_in_ring = points_per_ring - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parámetros u y v normalizados [0, 2π]
|
||||||
|
float u = (static_cast<float>(ring) / static_cast<float>(num_rings)) * 2.0f * PI;
|
||||||
|
float v = (static_cast<float>(point_in_ring) / static_cast<float>(points_per_ring)) * 2.0f * PI;
|
||||||
|
|
||||||
|
// Ecuaciones paramétricas del torus
|
||||||
|
// x = (R + r*cos(v)) * cos(u)
|
||||||
|
// y = (R + r*cos(v)) * sin(u)
|
||||||
|
// z = r * sin(v)
|
||||||
|
float cos_v = cosf(v);
|
||||||
|
float sin_v = sinf(v);
|
||||||
|
float cos_u = cosf(u);
|
||||||
|
float sin_u = sinf(u);
|
||||||
|
|
||||||
|
float radius_at_v = major_radius_ + (minor_radius_ * cos_v);
|
||||||
|
|
||||||
|
float x_base = radius_at_v * cos_u;
|
||||||
|
float y_base = radius_at_v * sin_u;
|
||||||
|
float z_base = minor_radius_ * sin_v;
|
||||||
|
|
||||||
|
// Aplicar rotación en eje X
|
||||||
|
float cos_x = cosf(angle_x_);
|
||||||
|
float sin_x = sinf(angle_x_);
|
||||||
|
float y_rot_x = (y_base * cos_x) - (z_base * sin_x);
|
||||||
|
float z_rot_x = (y_base * sin_x) + (z_base * cos_x);
|
||||||
|
|
||||||
|
// Aplicar rotación en eje Y
|
||||||
|
float cos_y = cosf(angle_y_);
|
||||||
|
float sin_y = sinf(angle_y_);
|
||||||
|
float x_rot_y = (x_base * cos_y) - (z_rot_x * sin_y);
|
||||||
|
float z_rot_y = (x_base * sin_y) + (z_rot_x * cos_y);
|
||||||
|
|
||||||
|
// Aplicar rotación en eje Z
|
||||||
|
float cos_z = cosf(angle_z_);
|
||||||
|
float sin_z = sinf(angle_z_);
|
||||||
|
float x_final = (x_rot_y * cos_z) - (y_rot_x * sin_z);
|
||||||
|
float y_final = (x_rot_y * sin_z) + (y_rot_x * cos_z);
|
||||||
|
|
||||||
|
// Retornar coordenadas finales rotadas
|
||||||
|
x = x_final;
|
||||||
|
y = y_final;
|
||||||
|
z = z_rot_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto TorusShape::getScaleFactor(float screen_height) const -> float {
|
||||||
|
// Factor de escala para física: proporcional al radio mayor
|
||||||
|
// Radio mayor base = 60px (0.25 * 240px en resolución 320x240)
|
||||||
|
const float BASE_RADIUS = 60.0f;
|
||||||
|
float current_radius = screen_height * TORUS_MAJOR_RADIUS_FACTOR;
|
||||||
|
return current_radius / BASE_RADIUS;
|
||||||
|
}
|
||||||
23
source/shapes/torus_shape.hpp
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "shape.hpp"
|
||||||
|
|
||||||
|
// Figura: Torus/Toroide 3D (donut/rosquilla)
|
||||||
|
// Comportamiento: Superficie toroidal con rotación triple (X, Y, Z)
|
||||||
|
// Ecuaciones: x = (R + r*cos(v))*cos(u), y = (R + r*cos(v))*sin(u), z = r*sin(v)
|
||||||
|
class TorusShape : public Shape {
|
||||||
|
private:
|
||||||
|
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
|
||||||
|
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
||||||
|
float angle_z_ = 0.0f; // Ángulo de rotación en eje Z (rad)
|
||||||
|
float major_radius_ = 0.0f; // Radio mayor R (del centro al tubo)
|
||||||
|
float minor_radius_ = 0.0f; // Radio menor r (grosor del tubo)
|
||||||
|
int num_points_ = 0; // Cantidad de puntos generados
|
||||||
|
|
||||||
|
public:
|
||||||
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
const char* getName() const override { return "TORUS"; }
|
||||||
|
float getScaleFactor(float screen_height) const override;
|
||||||
|
};
|
||||||
306
source/shapes_mgr/shape_manager.cpp
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
#include "shape_manager.hpp"
|
||||||
|
|
||||||
|
#include <algorithm> // for std::min, std::max, std::transform
|
||||||
|
#include <cctype> // for ::tolower
|
||||||
|
#include <cstdlib> // for rand
|
||||||
|
#include <string> // for std::string
|
||||||
|
|
||||||
|
#include "ball.hpp" // for Ball
|
||||||
|
#include "defines.hpp" // for constantes
|
||||||
|
#include "scene/scene_manager.hpp" // for SceneManager
|
||||||
|
#include "state/state_manager.hpp" // for StateManager
|
||||||
|
#include "ui/ui_manager.hpp" // for UIManager
|
||||||
|
|
||||||
|
// Includes de todas las shapes (necesario para creación polimórfica)
|
||||||
|
#include "shapes/atom_shape.hpp"
|
||||||
|
#include "shapes/cube_shape.hpp"
|
||||||
|
#include "shapes/cylinder_shape.hpp"
|
||||||
|
#include "shapes/helix_shape.hpp"
|
||||||
|
#include "shapes/icosahedron_shape.hpp"
|
||||||
|
#include "shapes/lissajous_shape.hpp"
|
||||||
|
#include "shapes/png_shape.hpp"
|
||||||
|
#include "shapes/sphere_shape.hpp"
|
||||||
|
#include "shapes/torus_shape.hpp"
|
||||||
|
|
||||||
|
ShapeManager::ShapeManager()
|
||||||
|
: engine_(nullptr),
|
||||||
|
scene_mgr_(nullptr),
|
||||||
|
ui_mgr_(nullptr),
|
||||||
|
state_mgr_(nullptr),
|
||||||
|
current_mode_(SimulationMode::PHYSICS),
|
||||||
|
current_shape_type_(ShapeType::SPHERE),
|
||||||
|
last_shape_type_(ShapeType::SPHERE),
|
||||||
|
active_shape_(nullptr),
|
||||||
|
shape_scale_factor_(1.0f),
|
||||||
|
depth_zoom_enabled_(true),
|
||||||
|
screen_width_(0),
|
||||||
|
screen_height_(0),
|
||||||
|
shape_convergence_(0.0f) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ShapeManager::~ShapeManager() = default;
|
||||||
|
|
||||||
|
void ShapeManager::initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, StateManager* state_mgr, int screen_width, int screen_height) {
|
||||||
|
engine_ = engine;
|
||||||
|
scene_mgr_ = scene_mgr;
|
||||||
|
ui_mgr_ = ui_mgr;
|
||||||
|
state_mgr_ = state_mgr;
|
||||||
|
screen_width_ = screen_width;
|
||||||
|
screen_height_ = screen_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShapeManager::updateScreenSize(int width, int height) {
|
||||||
|
screen_width_ = width;
|
||||||
|
screen_height_ = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// IMPLEMENTACIÓN COMPLETA - Migrado desde Engine
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void ShapeManager::toggleShapeMode(bool force_gravity_on_exit) {
|
||||||
|
if (current_mode_ == SimulationMode::PHYSICS) {
|
||||||
|
// Cambiar a modo figura (usar última figura seleccionada)
|
||||||
|
activateShapeInternal(last_shape_type_);
|
||||||
|
|
||||||
|
// Si estamos en modo LOGO y la figura es PNG_SHAPE, restaurar configuración LOGO
|
||||||
|
if ((state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::LOGO && last_shape_type_ == ShapeType::PNG_SHAPE) {
|
||||||
|
if (active_shape_) {
|
||||||
|
auto* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
|
||||||
|
if (png_shape != nullptr) {
|
||||||
|
png_shape->setLogoMode(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si estamos en LOGO MODE, resetear convergencia al entrar
|
||||||
|
if ((state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::LOGO) {
|
||||||
|
shape_convergence_ = 0.0f;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Volver a modo física normal
|
||||||
|
current_mode_ = SimulationMode::PHYSICS;
|
||||||
|
|
||||||
|
// Desactivar atracción y resetear escala de profundidad
|
||||||
|
scene_mgr_->enableShapeAttractionAll(false);
|
||||||
|
scene_mgr_->resetDepthScalesAll(); // Reset escala a 100% (evita "pop" visual)
|
||||||
|
|
||||||
|
// Activar gravedad al salir (solo si se especifica)
|
||||||
|
if (force_gravity_on_exit) {
|
||||||
|
scene_mgr_->forceBallsGravityOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar notificación (solo si NO estamos en modo demo o logo)
|
||||||
|
if ((state_mgr_ != nullptr) && (ui_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||||
|
ui_mgr_->showNotification("Modo física");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShapeManager::activateShape(ShapeType type) {
|
||||||
|
activateShapeInternal(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShapeManager::handleShapeScaleChange(bool increase) {
|
||||||
|
if (current_mode_ == SimulationMode::SHAPE) {
|
||||||
|
if (increase) {
|
||||||
|
shape_scale_factor_ += SHAPE_SCALE_STEP;
|
||||||
|
} else {
|
||||||
|
shape_scale_factor_ -= SHAPE_SCALE_STEP;
|
||||||
|
}
|
||||||
|
clampShapeScale();
|
||||||
|
|
||||||
|
// Mostrar notificación si está en modo SANDBOX
|
||||||
|
if ((ui_mgr_ != nullptr) && (state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||||
|
std::string notification = "Escala " + std::to_string(static_cast<int>((shape_scale_factor_ * 100.0f) + 0.5f)) + "%";
|
||||||
|
ui_mgr_->showNotification(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShapeManager::resetShapeScale() {
|
||||||
|
if (current_mode_ == SimulationMode::SHAPE) {
|
||||||
|
shape_scale_factor_ = SHAPE_SCALE_DEFAULT;
|
||||||
|
|
||||||
|
// Mostrar notificación si está en modo SANDBOX
|
||||||
|
if ((ui_mgr_ != nullptr) && (state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||||
|
ui_mgr_->showNotification("Escala 100%");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShapeManager::toggleDepthZoom() {
|
||||||
|
if (current_mode_ == SimulationMode::SHAPE) {
|
||||||
|
depth_zoom_enabled_ = !depth_zoom_enabled_;
|
||||||
|
|
||||||
|
// Mostrar notificación si está en modo SANDBOX
|
||||||
|
if ((ui_mgr_ != nullptr) && (state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||||
|
ui_mgr_->showNotification(depth_zoom_enabled_ ? "Profundidad on" : "Profundidad off");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShapeManager::update(float delta_time) {
|
||||||
|
if (!active_shape_ || current_mode_ != SimulationMode::SHAPE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar animación de la figura
|
||||||
|
active_shape_->update(delta_time, static_cast<float>(screen_width_), static_cast<float>(screen_height_));
|
||||||
|
|
||||||
|
// Obtener factor de escala para física (base de figura + escala manual)
|
||||||
|
float scale_factor = active_shape_->getScaleFactor(static_cast<float>(screen_height_)) * shape_scale_factor_;
|
||||||
|
|
||||||
|
// Centro de la pantalla
|
||||||
|
float center_x = screen_width_ / 2.0f;
|
||||||
|
float center_y = screen_height_ / 2.0f;
|
||||||
|
|
||||||
|
// Obtener referencia mutable a las bolas desde SceneManager
|
||||||
|
auto& balls = scene_mgr_->getBallsMutable();
|
||||||
|
|
||||||
|
// Actualizar cada pelota con física de atracción
|
||||||
|
for (size_t i = 0; i < balls.size(); i++) {
|
||||||
|
// Obtener posición 3D rotada del punto i
|
||||||
|
float x_3d;
|
||||||
|
float y_3d;
|
||||||
|
float z_3d;
|
||||||
|
active_shape_->getPoint3D(static_cast<int>(i), x_3d, y_3d, z_3d);
|
||||||
|
|
||||||
|
// Aplicar escala manual a las coordenadas 3D
|
||||||
|
x_3d *= shape_scale_factor_;
|
||||||
|
y_3d *= shape_scale_factor_;
|
||||||
|
z_3d *= shape_scale_factor_;
|
||||||
|
|
||||||
|
// Proyección 2D ortográfica (punto objetivo móvil)
|
||||||
|
float target_x = center_x + x_3d;
|
||||||
|
float target_y = center_y + y_3d;
|
||||||
|
|
||||||
|
// Actualizar target de la pelota para cálculo de convergencia
|
||||||
|
balls[i]->setShapeTarget2D(target_x, target_y);
|
||||||
|
|
||||||
|
// Aplicar fuerza de atracción física hacia el punto rotado
|
||||||
|
// Usar constantes SHAPE (mayor pegajosidad que ROTOBALL)
|
||||||
|
float shape_size = scale_factor * 80.0f; // 80px = radio base
|
||||||
|
balls[i]->applyShapeForce(target_x, target_y, shape_size, delta_time, SHAPE_SPRING_K, SHAPE_DAMPING_BASE, SHAPE_DAMPING_NEAR, SHAPE_NEAR_THRESHOLD, SHAPE_MAX_FORCE);
|
||||||
|
|
||||||
|
// Calcular brillo según profundidad Z para renderizado
|
||||||
|
// Normalizar Z al rango de la figura (asumiendo simetría ±shape_size)
|
||||||
|
float z_normalized = (z_3d + shape_size) / (2.0f * shape_size);
|
||||||
|
z_normalized = std::max(0.0f, std::min(1.0f, z_normalized));
|
||||||
|
balls[i]->setDepthBrightness(z_normalized);
|
||||||
|
|
||||||
|
// Calcular escala según profundidad Z (perspectiva) - solo si está activado
|
||||||
|
// 0.0 (fondo) → 0.5x, 0.5 (medio) → 1.0x, 1.0 (frente) → 1.5x
|
||||||
|
float depth_scale = depth_zoom_enabled_ ? (0.5f + (z_normalized * 1.0f)) : 1.0f;
|
||||||
|
balls[i]->setDepthScale(depth_scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular convergencia en LOGO MODE (% de pelotas cerca de su objetivo)
|
||||||
|
if ((state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::LOGO && current_mode_ == SimulationMode::SHAPE) {
|
||||||
|
int balls_near = 0;
|
||||||
|
float distance_threshold = LOGO_CONVERGENCE_DISTANCE; // 20px fijo (más permisivo)
|
||||||
|
|
||||||
|
for (const auto& ball : balls) {
|
||||||
|
if (ball->getDistanceToTarget() < distance_threshold) {
|
||||||
|
balls_near++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shape_convergence_ = static_cast<float>(balls_near) / scene_mgr_->getBallCount();
|
||||||
|
|
||||||
|
// Notificar a la figura sobre el porcentaje de convergencia
|
||||||
|
// Esto permite que PNGShape decida cuándo empezar a contar para flips
|
||||||
|
active_shape_->setConvergence(shape_convergence_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShapeManager::generateShape() {
|
||||||
|
if (!active_shape_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int num_points = static_cast<int>(scene_mgr_->getBallCount());
|
||||||
|
active_shape_->generatePoints(num_points, static_cast<float>(screen_width_), static_cast<float>(screen_height_));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MÉTODOS PRIVADOS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void ShapeManager::activateShapeInternal(ShapeType type) {
|
||||||
|
// Guardar como última figura seleccionada
|
||||||
|
last_shape_type_ = type;
|
||||||
|
current_shape_type_ = type;
|
||||||
|
|
||||||
|
// Cambiar a modo figura
|
||||||
|
current_mode_ = SimulationMode::SHAPE;
|
||||||
|
|
||||||
|
// Desactivar gravedad al entrar en modo figura
|
||||||
|
scene_mgr_->forceBallsGravityOff();
|
||||||
|
|
||||||
|
// Crear instancia polimórfica de la figura correspondiente
|
||||||
|
switch (type) {
|
||||||
|
case ShapeType::SPHERE:
|
||||||
|
active_shape_ = std::make_unique<SphereShape>();
|
||||||
|
break;
|
||||||
|
case ShapeType::CUBE:
|
||||||
|
active_shape_ = std::make_unique<CubeShape>();
|
||||||
|
break;
|
||||||
|
case ShapeType::HELIX:
|
||||||
|
active_shape_ = std::make_unique<HelixShape>();
|
||||||
|
break;
|
||||||
|
case ShapeType::TORUS:
|
||||||
|
active_shape_ = std::make_unique<TorusShape>();
|
||||||
|
break;
|
||||||
|
case ShapeType::LISSAJOUS:
|
||||||
|
active_shape_ = std::make_unique<LissajousShape>();
|
||||||
|
break;
|
||||||
|
case ShapeType::CYLINDER:
|
||||||
|
active_shape_ = std::make_unique<CylinderShape>();
|
||||||
|
break;
|
||||||
|
case ShapeType::ICOSAHEDRON:
|
||||||
|
active_shape_ = std::make_unique<IcosahedronShape>();
|
||||||
|
break;
|
||||||
|
case ShapeType::ATOM:
|
||||||
|
active_shape_ = std::make_unique<AtomShape>();
|
||||||
|
break;
|
||||||
|
case ShapeType::PNG_SHAPE:
|
||||||
|
active_shape_ = std::make_unique<PNGShape>("shapes/jailgames.png");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
active_shape_ = std::make_unique<SphereShape>(); // Fallback
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generar puntos de la figura
|
||||||
|
generateShape();
|
||||||
|
|
||||||
|
// Activar atracción física en todas las pelotas
|
||||||
|
scene_mgr_->enableShapeAttractionAll(true);
|
||||||
|
|
||||||
|
// Mostrar notificación con nombre de figura (solo si NO estamos en modo demo o logo)
|
||||||
|
if (active_shape_ && (state_mgr_ != nullptr) && (ui_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||||
|
std::string shape_name = active_shape_->getName();
|
||||||
|
std::ranges::transform(shape_name, shape_name.begin(), ::tolower);
|
||||||
|
std::string notification = std::string("Modo ") + shape_name;
|
||||||
|
ui_mgr_->showNotification(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShapeManager::setShapeScaleFactor(float scale) {
|
||||||
|
shape_scale_factor_ = scale;
|
||||||
|
clampShapeScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShapeManager::clampShapeScale() {
|
||||||
|
// Calcular tamaño máximo permitido según resolución actual
|
||||||
|
// La figura más grande (esfera/cubo) usa ~33% de altura por defecto
|
||||||
|
// Permitir hasta que la figura ocupe 90% de la dimensión más pequeña
|
||||||
|
float max_dimension = std::min(screen_width_, screen_height_);
|
||||||
|
float base_size_factor = 0.333f; // ROTOBALL_RADIUS_FACTOR o similar
|
||||||
|
float max_scale_for_screen = (max_dimension * 0.9f) / (max_dimension * base_size_factor);
|
||||||
|
|
||||||
|
// Limitar entre SHAPE_SCALE_MIN y el mínimo de (SHAPE_SCALE_MAX, max_scale_for_screen)
|
||||||
|
float max_allowed = std::min(SHAPE_SCALE_MAX, max_scale_for_screen);
|
||||||
|
shape_scale_factor_ = std::max(SHAPE_SCALE_MIN, std::min(max_allowed, shape_scale_factor_));
|
||||||
|
}
|
||||||
181
source/shapes_mgr/shape_manager.hpp
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory> // for unique_ptr
|
||||||
|
|
||||||
|
#include "defines.hpp" // for SimulationMode, ShapeType
|
||||||
|
#include "shapes/shape.hpp" // for Shape base class
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
class Engine;
|
||||||
|
class SceneManager;
|
||||||
|
class UIManager;
|
||||||
|
class StateManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class ShapeManager
|
||||||
|
* @brief Gestiona el sistema de figuras 3D (esferas, cubos, PNG shapes, etc.)
|
||||||
|
*
|
||||||
|
* Responsabilidad única: Gestión de figuras 3D polimórficas
|
||||||
|
*
|
||||||
|
* Características:
|
||||||
|
* - Control de modo simulación (PHYSICS/SHAPE)
|
||||||
|
* - Gestión de tipos de figura (SPHERE/CUBE/PYRAMID/TORUS/ICOSAHEDRON/PNG_SHAPE)
|
||||||
|
* - Sistema de escalado manual (Numpad +/-)
|
||||||
|
* - Toggle de depth zoom (Z)
|
||||||
|
* - Generación y actualización de puntos de figura
|
||||||
|
* - Callbacks al Engine para renderizado
|
||||||
|
*/
|
||||||
|
class ShapeManager {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Constructor
|
||||||
|
*/
|
||||||
|
ShapeManager();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Destructor
|
||||||
|
*/
|
||||||
|
~ShapeManager();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Inicializa el ShapeManager con referencias a otros componentes
|
||||||
|
* @param engine Puntero al Engine (para callbacks legacy)
|
||||||
|
* @param scene_mgr Puntero a SceneManager (para acceso a bolas)
|
||||||
|
* @param ui_mgr Puntero a UIManager (para notificaciones)
|
||||||
|
* @param state_mgr Puntero a StateManager (para verificar modo actual)
|
||||||
|
* @param screen_width Ancho lógico de pantalla
|
||||||
|
* @param screen_height Alto lógico de pantalla
|
||||||
|
*/
|
||||||
|
void initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, StateManager* state_mgr, int screen_width, int screen_height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Toggle entre modo PHYSICS y SHAPE
|
||||||
|
* @param force_gravity_on_exit Forzar gravedad al salir de SHAPE mode
|
||||||
|
*/
|
||||||
|
void toggleShapeMode(bool force_gravity_on_exit = true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Activa un tipo específico de figura
|
||||||
|
* @param type Tipo de figura a activar
|
||||||
|
*/
|
||||||
|
void activateShape(ShapeType type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Cambia la escala de la figura actual
|
||||||
|
* @param increase true para aumentar, false para reducir
|
||||||
|
*/
|
||||||
|
void handleShapeScaleChange(bool increase);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Resetea la escala de figura a 1.0
|
||||||
|
*/
|
||||||
|
void resetShapeScale();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Toggle del zoom por profundidad Z
|
||||||
|
*/
|
||||||
|
void toggleDepthZoom();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Actualiza la figura activa (rotación, etc.)
|
||||||
|
* @param delta_time Delta time para animaciones
|
||||||
|
*/
|
||||||
|
void update(float delta_time);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Genera los puntos de la figura activa
|
||||||
|
*/
|
||||||
|
void generateShape();
|
||||||
|
|
||||||
|
// === Getters ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Obtiene el modo de simulación actual
|
||||||
|
*/
|
||||||
|
SimulationMode getCurrentMode() const { return current_mode_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Obtiene el tipo de figura actual
|
||||||
|
*/
|
||||||
|
ShapeType getCurrentShapeType() const { return current_shape_type_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Obtiene puntero a la figura activa
|
||||||
|
*/
|
||||||
|
Shape* getActiveShape() { return active_shape_.get(); }
|
||||||
|
const Shape* getActiveShape() const { return active_shape_.get(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Obtiene el factor de escala actual
|
||||||
|
*/
|
||||||
|
float getShapeScaleFactor() const { return shape_scale_factor_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Verifica si depth zoom está activado
|
||||||
|
*/
|
||||||
|
bool isDepthZoomEnabled() const { return depth_zoom_enabled_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Verifica si modo SHAPE está activo
|
||||||
|
*/
|
||||||
|
bool isShapeModeActive() const { return current_mode_ == SimulationMode::SHAPE; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Actualiza el tamaño de pantalla (para resize/fullscreen)
|
||||||
|
* @param width Nuevo ancho lógico
|
||||||
|
* @param height Nuevo alto lógico
|
||||||
|
*/
|
||||||
|
void updateScreenSize(int width, int height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Obtiene convergencia actual (para modo LOGO)
|
||||||
|
*/
|
||||||
|
float getConvergence() const { return shape_convergence_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Establece la escala de figura y aplica clamping
|
||||||
|
* @param scale Nuevo factor de escala (se limitará a rango válido)
|
||||||
|
*/
|
||||||
|
void setShapeScaleFactor(float scale);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Establece el estado del zoom por profundidad Z
|
||||||
|
* @param enabled true para activar, false para desactivar
|
||||||
|
*/
|
||||||
|
void setDepthZoomEnabled(bool enabled) { depth_zoom_enabled_ = enabled; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// === Referencias a otros componentes ===
|
||||||
|
Engine* engine_; // Callback al Engine (legacy - temporal)
|
||||||
|
SceneManager* scene_mgr_; // Acceso a bolas y física
|
||||||
|
UIManager* ui_mgr_; // Notificaciones
|
||||||
|
StateManager* state_mgr_; // Verificación de modo actual
|
||||||
|
|
||||||
|
// === Estado de figuras 3D ===
|
||||||
|
SimulationMode current_mode_;
|
||||||
|
ShapeType current_shape_type_;
|
||||||
|
ShapeType last_shape_type_;
|
||||||
|
std::unique_ptr<Shape> active_shape_;
|
||||||
|
float shape_scale_factor_;
|
||||||
|
bool depth_zoom_enabled_;
|
||||||
|
|
||||||
|
// === Dimensiones de pantalla ===
|
||||||
|
int screen_width_;
|
||||||
|
int screen_height_;
|
||||||
|
|
||||||
|
// === Convergencia (para modo LOGO) ===
|
||||||
|
float shape_convergence_;
|
||||||
|
|
||||||
|
// === Métodos privados ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Implementación interna de activación de figura
|
||||||
|
* @param type Tipo de figura
|
||||||
|
*/
|
||||||
|
void activateShapeInternal(ShapeType type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Limita la escala para evitar clipping
|
||||||
|
*/
|
||||||
|
void clampShapeScale();
|
||||||
|
};
|
||||||
660
source/state/state_manager.cpp
Normal file
@@ -0,0 +1,660 @@
|
|||||||
|
#include "state_manager.hpp"
|
||||||
|
|
||||||
|
#include <algorithm> // for std::min
|
||||||
|
#include <array> // for std::array
|
||||||
|
#include <cstdlib> // for rand
|
||||||
|
#include <vector> // for std::vector
|
||||||
|
|
||||||
|
#include "defines.hpp" // for constantes DEMO/LOGO
|
||||||
|
#include "engine.hpp" // for Engine (enter/exitShapeMode, texture)
|
||||||
|
#include "scene/scene_manager.hpp" // for SceneManager
|
||||||
|
#include "shapes/png_shape.hpp" // for PNGShape flip detection
|
||||||
|
#include "shapes_mgr/shape_manager.hpp" // for ShapeManager
|
||||||
|
#include "theme_manager.hpp" // for ThemeManager
|
||||||
|
|
||||||
|
StateManager::StateManager()
|
||||||
|
: engine_(nullptr),
|
||||||
|
scene_mgr_(nullptr),
|
||||||
|
theme_mgr_(nullptr),
|
||||||
|
shape_mgr_(nullptr),
|
||||||
|
current_app_mode_(AppMode::SANDBOX),
|
||||||
|
previous_app_mode_(AppMode::SANDBOX),
|
||||||
|
demo_timer_(0.0f),
|
||||||
|
demo_next_action_time_(0.0f),
|
||||||
|
logo_convergence_threshold_(0.90f),
|
||||||
|
logo_min_time_(3.0f),
|
||||||
|
logo_max_time_(5.0f),
|
||||||
|
logo_waiting_for_flip_(false),
|
||||||
|
logo_target_flip_number_(0),
|
||||||
|
logo_target_flip_percentage_(0.0f),
|
||||||
|
logo_current_flip_count_(0),
|
||||||
|
logo_entered_manually_(false),
|
||||||
|
logo_previous_theme_(0),
|
||||||
|
logo_previous_texture_index_(0),
|
||||||
|
logo_previous_shape_scale_(1.0f) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void StateManager::initialize(Engine* engine, SceneManager* scene_mgr, ThemeManager* theme_mgr, ShapeManager* shape_mgr) {
|
||||||
|
engine_ = engine;
|
||||||
|
scene_mgr_ = scene_mgr;
|
||||||
|
theme_mgr_ = theme_mgr;
|
||||||
|
shape_mgr_ = shape_mgr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StateManager::setLogoPreviousState(int theme, size_t texture_index, float shape_scale) {
|
||||||
|
logo_previous_theme_ = theme;
|
||||||
|
logo_previous_texture_index_ = texture_index;
|
||||||
|
logo_previous_shape_scale_ = shape_scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// ACTUALIZACIÓN DE ESTADOS
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
void StateManager::update(float delta_time, float shape_convergence, Shape* active_shape) { // NOLINT(readability-function-cognitive-complexity)
|
||||||
|
if (current_app_mode_ == AppMode::SANDBOX) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
demo_timer_ += delta_time;
|
||||||
|
|
||||||
|
bool should_trigger = false;
|
||||||
|
|
||||||
|
if (current_app_mode_ == AppMode::LOGO) {
|
||||||
|
if (logo_waiting_for_flip_) {
|
||||||
|
// CAMINO B: Esperando a que ocurran flips
|
||||||
|
auto* png_shape = dynamic_cast<PNGShape*>(active_shape);
|
||||||
|
|
||||||
|
if (png_shape != nullptr) {
|
||||||
|
int current_flip_count = png_shape->getFlipCount();
|
||||||
|
|
||||||
|
logo_current_flip_count_ = std::max(current_flip_count, logo_current_flip_count_);
|
||||||
|
|
||||||
|
if (logo_current_flip_count_ + 1 >= logo_target_flip_number_) {
|
||||||
|
if (png_shape->isFlipping()) {
|
||||||
|
float flip_progress = png_shape->getFlipProgress();
|
||||||
|
if (flip_progress >= logo_target_flip_percentage_) {
|
||||||
|
should_trigger = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// CAMINO A: Esperar convergencia + tiempo
|
||||||
|
bool min_time_reached = demo_timer_ >= logo_min_time_;
|
||||||
|
bool max_time_reached = demo_timer_ >= logo_max_time_;
|
||||||
|
bool convergence_ok = shape_convergence >= logo_convergence_threshold_;
|
||||||
|
|
||||||
|
should_trigger = (min_time_reached && convergence_ok) || max_time_reached;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
should_trigger = demo_timer_ >= demo_next_action_time_;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!should_trigger) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_app_mode_ == AppMode::LOGO) {
|
||||||
|
// LOGO MODE: Sistema de acciones variadas con gravedad dinámica
|
||||||
|
int action = rand() % 100;
|
||||||
|
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
// Logo quieto (formado) → Decidir camino a seguir
|
||||||
|
if (logo_waiting_for_flip_) {
|
||||||
|
// Ya estábamos esperando flips → hacer el cambio SHAPE → PHYSICS
|
||||||
|
if (action < 50) {
|
||||||
|
engine_->exitShapeMode(true); // Con gravedad ON
|
||||||
|
} else {
|
||||||
|
engine_->exitShapeMode(false); // Con gravedad OFF
|
||||||
|
}
|
||||||
|
|
||||||
|
logo_waiting_for_flip_ = false;
|
||||||
|
logo_current_flip_count_ = 0;
|
||||||
|
|
||||||
|
demo_timer_ = 0.0f;
|
||||||
|
float interval_range = logo_max_time_ - logo_min_time_;
|
||||||
|
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
|
||||||
|
} else if (rand() % 100 < LOGO_FLIP_WAIT_PROBABILITY) {
|
||||||
|
// CAMINO B (50%): Esperar flips
|
||||||
|
logo_waiting_for_flip_ = true;
|
||||||
|
logo_target_flip_number_ = LOGO_FLIP_WAIT_MIN + rand() % (LOGO_FLIP_WAIT_MAX - LOGO_FLIP_WAIT_MIN + 1);
|
||||||
|
logo_target_flip_percentage_ = LOGO_FLIP_TRIGGER_MIN + (rand() % 1000) / 1000.0f * (LOGO_FLIP_TRIGGER_MAX - LOGO_FLIP_TRIGGER_MIN);
|
||||||
|
logo_current_flip_count_ = 0;
|
||||||
|
|
||||||
|
auto* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
|
||||||
|
if (png_shape != nullptr) {
|
||||||
|
png_shape->resetFlipCount();
|
||||||
|
}
|
||||||
|
// No hacer nada más — esperar a que ocurran los flips
|
||||||
|
} else {
|
||||||
|
// CAMINO A (50%): Cambio inmediato
|
||||||
|
if (action < 50) {
|
||||||
|
engine_->exitShapeMode(true); // SHAPE → PHYSICS con gravedad ON
|
||||||
|
} else {
|
||||||
|
engine_->exitShapeMode(false); // SHAPE → PHYSICS con gravedad OFF
|
||||||
|
}
|
||||||
|
|
||||||
|
logo_waiting_for_flip_ = false;
|
||||||
|
logo_current_flip_count_ = 0;
|
||||||
|
|
||||||
|
demo_timer_ = 0.0f;
|
||||||
|
float interval_range = logo_max_time_ - logo_min_time_;
|
||||||
|
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Logo animado (PHYSICS) → 4 opciones
|
||||||
|
if (action < 50) {
|
||||||
|
// 50%: PHYSICS → SHAPE (reconstruir logo)
|
||||||
|
engine_->exitShapeMode(false); // toggleShapeMode: PHYSICS → SHAPE con last_type
|
||||||
|
|
||||||
|
logo_waiting_for_flip_ = false;
|
||||||
|
logo_current_flip_count_ = 0;
|
||||||
|
} else if (action < 68) {
|
||||||
|
// 18%: Forzar gravedad ON
|
||||||
|
scene_mgr_->forceBallsGravityOn();
|
||||||
|
} else if (action < 84) {
|
||||||
|
// 16%: Forzar gravedad OFF
|
||||||
|
scene_mgr_->forceBallsGravityOff();
|
||||||
|
} else {
|
||||||
|
// 16%: Cambiar dirección de gravedad
|
||||||
|
auto new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||||
|
scene_mgr_->changeGravityDirection(new_direction);
|
||||||
|
scene_mgr_->forceBallsGravityOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
demo_timer_ = 0.0f;
|
||||||
|
float interval_range = logo_max_time_ - logo_min_time_;
|
||||||
|
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salir automáticamente si la entrada fue automática (desde DEMO)
|
||||||
|
if (!logo_entered_manually_ && rand() % 100 < 60) {
|
||||||
|
exitLogoMode(true); // Volver a DEMO/DEMO_LITE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// DEMO/DEMO_LITE: Acciones normales
|
||||||
|
bool is_lite = (current_app_mode_ == AppMode::DEMO_LITE);
|
||||||
|
performDemoAction(is_lite);
|
||||||
|
|
||||||
|
demo_timer_ = 0.0f;
|
||||||
|
float interval_min = is_lite ? DEMO_LITE_ACTION_INTERVAL_MIN : DEMO_ACTION_INTERVAL_MIN;
|
||||||
|
float interval_max = is_lite ? DEMO_LITE_ACTION_INTERVAL_MAX : DEMO_ACTION_INTERVAL_MAX;
|
||||||
|
float interval_range = interval_max - interval_min;
|
||||||
|
demo_next_action_time_ = interval_min + (rand() % 1000) / 1000.0f * interval_range;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StateManager::setState(AppMode new_mode, int current_screen_width, int current_screen_height) {
|
||||||
|
if (current_app_mode_ == new_mode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_app_mode_ == AppMode::LOGO && new_mode != AppMode::LOGO) {
|
||||||
|
previous_app_mode_ = new_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_mode == AppMode::LOGO) {
|
||||||
|
previous_app_mode_ = current_app_mode_;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_app_mode_ = new_mode;
|
||||||
|
|
||||||
|
demo_timer_ = 0.0f;
|
||||||
|
|
||||||
|
if (new_mode == AppMode::DEMO || new_mode == AppMode::DEMO_LITE || new_mode == AppMode::LOGO) {
|
||||||
|
float min_interval;
|
||||||
|
float max_interval;
|
||||||
|
|
||||||
|
if (new_mode == AppMode::LOGO) {
|
||||||
|
float resolution_scale = current_screen_height / 720.0f;
|
||||||
|
logo_min_time_ = LOGO_ACTION_INTERVAL_MIN * resolution_scale;
|
||||||
|
logo_max_time_ = LOGO_ACTION_INTERVAL_MAX * resolution_scale;
|
||||||
|
|
||||||
|
min_interval = logo_min_time_;
|
||||||
|
max_interval = logo_max_time_;
|
||||||
|
} else {
|
||||||
|
bool is_lite = (new_mode == AppMode::DEMO_LITE);
|
||||||
|
min_interval = is_lite ? DEMO_LITE_ACTION_INTERVAL_MIN : DEMO_ACTION_INTERVAL_MIN;
|
||||||
|
max_interval = is_lite ? DEMO_LITE_ACTION_INTERVAL_MAX : DEMO_ACTION_INTERVAL_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
demo_next_action_time_ = min_interval + (rand() % 1000) / 1000.0f * (max_interval - min_interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StateManager::toggleDemoMode(int current_screen_width, int current_screen_height) {
|
||||||
|
if (current_app_mode_ == AppMode::DEMO) {
|
||||||
|
setState(AppMode::SANDBOX, current_screen_width, current_screen_height);
|
||||||
|
} else {
|
||||||
|
setState(AppMode::DEMO, current_screen_width, current_screen_height);
|
||||||
|
randomizeOnDemoStart(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StateManager::toggleDemoLiteMode(int current_screen_width, int current_screen_height) {
|
||||||
|
if (current_app_mode_ == AppMode::DEMO_LITE) {
|
||||||
|
setState(AppMode::SANDBOX, current_screen_width, current_screen_height);
|
||||||
|
} else {
|
||||||
|
setState(AppMode::DEMO_LITE, current_screen_width, current_screen_height);
|
||||||
|
randomizeOnDemoStart(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StateManager::toggleLogoMode(int current_screen_width, int current_screen_height, size_t ball_count) {
|
||||||
|
if (current_app_mode_ == AppMode::LOGO) {
|
||||||
|
exitLogoMode(false);
|
||||||
|
} else {
|
||||||
|
enterLogoMode(false, current_screen_width, current_screen_height, ball_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// ACCIONES DE DEMO
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
void StateManager::performDemoAction(bool is_lite) { // NOLINT(readability-function-cognitive-complexity)
|
||||||
|
if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// SALTO AUTOMÁTICO A LOGO MODE (Easter Egg)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
if (is_lite) {
|
||||||
|
if (static_cast<int>(scene_mgr_->getBallCount()) >= BALL_COUNT_SCENARIOS[LOGO_MIN_SCENARIO_IDX] &&
|
||||||
|
theme_mgr_->getCurrentThemeIndex() == 5) { // MONOCHROME
|
||||||
|
if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO_LITE) {
|
||||||
|
enterLogoMode(true, 0, 0, scene_mgr_->getBallCount());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (static_cast<int>(scene_mgr_->getBallCount()) >= BALL_COUNT_SCENARIOS[LOGO_MIN_SCENARIO_IDX]) {
|
||||||
|
if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO) {
|
||||||
|
enterLogoMode(true, 0, 0, scene_mgr_->getBallCount());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ACCIONES NORMALES DE DEMO/DEMO_LITE
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
int total_weight;
|
||||||
|
int random_value;
|
||||||
|
int accumulated_weight = 0;
|
||||||
|
|
||||||
|
if (is_lite) {
|
||||||
|
total_weight = DEMO_LITE_WEIGHT_GRAVITY_DIR + DEMO_LITE_WEIGHT_GRAVITY_TOGGLE + DEMO_LITE_WEIGHT_SHAPE + DEMO_LITE_WEIGHT_TOGGLE_PHYSICS + DEMO_LITE_WEIGHT_IMPULSE;
|
||||||
|
random_value = rand() % total_weight;
|
||||||
|
|
||||||
|
// Cambiar dirección gravedad (25%)
|
||||||
|
accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_DIR;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
auto new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||||
|
scene_mgr_->changeGravityDirection(new_direction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle gravedad ON/OFF (20%)
|
||||||
|
accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_TOGGLE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
toggleGravityOnOff();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activar figura 3D (25%) - PNG_SHAPE excluido
|
||||||
|
accumulated_weight += DEMO_LITE_WEIGHT_SHAPE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
||||||
|
engine_->enterShapeMode(SHAPES[rand() % 8]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle física ↔ figura (20%)
|
||||||
|
accumulated_weight += DEMO_LITE_WEIGHT_TOGGLE_PHYSICS;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
engine_->exitShapeMode(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar impulso (10%)
|
||||||
|
accumulated_weight += DEMO_LITE_WEIGHT_IMPULSE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
scene_mgr_->pushBallsAwayFromGravity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
total_weight = DEMO_WEIGHT_GRAVITY_DIR + DEMO_WEIGHT_GRAVITY_TOGGLE + DEMO_WEIGHT_SHAPE + DEMO_WEIGHT_TOGGLE_PHYSICS + DEMO_WEIGHT_REGENERATE_SHAPE + DEMO_WEIGHT_THEME + DEMO_WEIGHT_SCENARIO + DEMO_WEIGHT_IMPULSE + DEMO_WEIGHT_DEPTH_ZOOM + DEMO_WEIGHT_SHAPE_SCALE + DEMO_WEIGHT_SPRITE;
|
||||||
|
random_value = rand() % total_weight;
|
||||||
|
|
||||||
|
// Cambiar dirección gravedad (10%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_GRAVITY_DIR;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
auto new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||||
|
scene_mgr_->changeGravityDirection(new_direction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle gravedad ON/OFF (8%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_GRAVITY_TOGGLE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
toggleGravityOnOff();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activar figura 3D (20%) - PNG_SHAPE excluido
|
||||||
|
accumulated_weight += DEMO_WEIGHT_SHAPE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
||||||
|
engine_->enterShapeMode(SHAPES[rand() % 8]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle física ↔ figura (12%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_TOGGLE_PHYSICS;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
engine_->exitShapeMode(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-generar misma figura (8%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_REGENERATE_SHAPE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
shape_mgr_->generateShape();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambiar tema (15%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_THEME;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
theme_mgr_->switchToTheme(rand() % 15);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambiar escenario (10%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_SCENARIO;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
int auto_max = std::min(engine_->getMaxAutoScenario(), DEMO_AUTO_MAX_SCENARIO);
|
||||||
|
std::vector<int> candidates;
|
||||||
|
for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) {
|
||||||
|
candidates.push_back(i);
|
||||||
|
}
|
||||||
|
if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable()) {
|
||||||
|
candidates.push_back(CUSTOM_SCENARIO_IDX);
|
||||||
|
}
|
||||||
|
int new_scenario = candidates[rand() % candidates.size()];
|
||||||
|
SimulationMode current_sim_mode = shape_mgr_->getCurrentMode();
|
||||||
|
scene_mgr_->changeScenario(new_scenario, current_sim_mode);
|
||||||
|
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
shape_mgr_->generateShape();
|
||||||
|
scene_mgr_->enableShapeAttractionAll(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar impulso (10%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_IMPULSE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
scene_mgr_->pushBallsAwayFromGravity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle profundidad (3%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_DEPTH_ZOOM;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
shape_mgr_->setDepthZoomEnabled(!shape_mgr_->isDepthZoomEnabled());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambiar escala de figura (2%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_SHAPE_SCALE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
int scale_action = rand() % 3;
|
||||||
|
if (scale_action == 0) {
|
||||||
|
shape_mgr_->setShapeScaleFactor(shape_mgr_->getShapeScaleFactor() + SHAPE_SCALE_STEP);
|
||||||
|
} else if (scale_action == 1) {
|
||||||
|
shape_mgr_->setShapeScaleFactor(shape_mgr_->getShapeScaleFactor() - SHAPE_SCALE_STEP);
|
||||||
|
} else {
|
||||||
|
shape_mgr_->setShapeScaleFactor(SHAPE_SCALE_DEFAULT);
|
||||||
|
}
|
||||||
|
shape_mgr_->generateShape();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambiar sprite (2%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_SPRITE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
engine_->switchTextureSilent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// RANDOMIZACIÓN AL INICIAR DEMO
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
void StateManager::randomizeOnDemoStart(bool is_lite) { // NOLINT(readability-function-cognitive-complexity)
|
||||||
|
if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si venimos de LOGO con PNG_SHAPE, cambiar figura obligatoriamente
|
||||||
|
if (shape_mgr_->getCurrentShapeType() == ShapeType::PNG_SHAPE) {
|
||||||
|
constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
||||||
|
engine_->enterShapeMode(SHAPES[rand() % 8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_lite) {
|
||||||
|
// DEMO LITE: Solo randomizar física/figura + gravedad
|
||||||
|
if (rand() % 2 == 0) {
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
engine_->exitShapeMode(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
||||||
|
engine_->enterShapeMode(SHAPES[rand() % 8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||||
|
scene_mgr_->changeGravityDirection(new_direction);
|
||||||
|
if (rand() % 2 == 0) {
|
||||||
|
toggleGravityOnOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// DEMO COMPLETO: Randomizar TODO
|
||||||
|
|
||||||
|
// 1. Física o Figura
|
||||||
|
if (rand() % 2 == 0) {
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
engine_->exitShapeMode(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
||||||
|
ShapeType selected_shape = SHAPES[rand() % 8];
|
||||||
|
|
||||||
|
// Randomizar profundidad y escala ANTES de activar la figura
|
||||||
|
if (rand() % 2 == 0) {
|
||||||
|
shape_mgr_->setDepthZoomEnabled(!shape_mgr_->isDepthZoomEnabled());
|
||||||
|
}
|
||||||
|
shape_mgr_->setShapeScaleFactor(0.5f + ((rand() % 1500) / 1000.0f));
|
||||||
|
|
||||||
|
engine_->enterShapeMode(selected_shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Escenario
|
||||||
|
int auto_max = std::min(engine_->getMaxAutoScenario(), DEMO_AUTO_MAX_SCENARIO);
|
||||||
|
std::vector<int> candidates;
|
||||||
|
for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) {
|
||||||
|
candidates.push_back(i);
|
||||||
|
}
|
||||||
|
if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable()) {
|
||||||
|
candidates.push_back(CUSTOM_SCENARIO_IDX);
|
||||||
|
}
|
||||||
|
int new_scenario = candidates[rand() % candidates.size()];
|
||||||
|
SimulationMode current_sim_mode = shape_mgr_->getCurrentMode();
|
||||||
|
scene_mgr_->changeScenario(new_scenario, current_sim_mode);
|
||||||
|
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
shape_mgr_->generateShape();
|
||||||
|
scene_mgr_->enableShapeAttractionAll(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Tema
|
||||||
|
theme_mgr_->switchToTheme(rand() % 15);
|
||||||
|
|
||||||
|
// 4. Sprite
|
||||||
|
if (rand() % 2 == 0) {
|
||||||
|
engine_->switchTextureSilent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Gravedad
|
||||||
|
auto new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||||
|
scene_mgr_->changeGravityDirection(new_direction);
|
||||||
|
if (rand() % 3 == 0) {
|
||||||
|
toggleGravityOnOff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// TOGGLE GRAVEDAD (para DEMO)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
void StateManager::toggleGravityOnOff() {
|
||||||
|
if (scene_mgr_ == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool gravity_enabled = scene_mgr_->hasBalls() &&
|
||||||
|
(scene_mgr_->getFirstBall()->getGravityForce() > 0.0f);
|
||||||
|
|
||||||
|
if (gravity_enabled) {
|
||||||
|
scene_mgr_->forceBallsGravityOff();
|
||||||
|
} else {
|
||||||
|
scene_mgr_->forceBallsGravityOn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// ENTRAR AL MODO LOGO
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int current_screen_height, size_t ball_count) {
|
||||||
|
if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logo_entered_manually_ = !from_demo;
|
||||||
|
|
||||||
|
logo_waiting_for_flip_ = false;
|
||||||
|
logo_target_flip_number_ = 0;
|
||||||
|
logo_target_flip_percentage_ = 0.0f;
|
||||||
|
logo_current_flip_count_ = 0;
|
||||||
|
|
||||||
|
// Cambiar a modo LOGO (guarda previous_app_mode_ automáticamente)
|
||||||
|
setState(AppMode::LOGO, current_screen_width, current_screen_height);
|
||||||
|
|
||||||
|
// Verificar mínimo de pelotas
|
||||||
|
if (scene_mgr_->getCurrentScenario() < LOGO_MIN_SCENARIO_IDX) {
|
||||||
|
scene_mgr_->changeScenario(LOGO_MIN_SCENARIO_IDX, shape_mgr_->getCurrentMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardar estado previo (para restaurar al salir)
|
||||||
|
logo_previous_theme_ = theme_mgr_->getCurrentThemeIndex();
|
||||||
|
logo_previous_texture_index_ = engine_->getCurrentTextureIndex();
|
||||||
|
logo_previous_shape_scale_ = shape_mgr_->getShapeScaleFactor();
|
||||||
|
|
||||||
|
// Aplicar textura "small" si existe — buscar por nombre iterando índices
|
||||||
|
if (engine_->getCurrentTextureName() != "small") {
|
||||||
|
size_t original_idx = logo_previous_texture_index_;
|
||||||
|
bool found = false;
|
||||||
|
for (size_t i = 0; i < 20; ++i) {
|
||||||
|
engine_->setTextureByIndex(i);
|
||||||
|
if (engine_->getCurrentTextureName() == "small") {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (engine_->getCurrentTextureName().empty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
engine_->setTextureByIndex(original_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambiar a tema aleatorio entre: MONOCHROME, LAVENDER, CRIMSON, ESMERALDA
|
||||||
|
constexpr std::array<int, 4> LOGO_THEMES = {5, 6, 7, 8};
|
||||||
|
theme_mgr_->switchToTheme(LOGO_THEMES[rand() % 4]);
|
||||||
|
|
||||||
|
// Establecer escala a 120%
|
||||||
|
shape_mgr_->setShapeScaleFactor(LOGO_MODE_SHAPE_SCALE);
|
||||||
|
|
||||||
|
// Activar PNG_SHAPE (el logo)
|
||||||
|
engine_->enterShapeMode(ShapeType::PNG_SHAPE);
|
||||||
|
|
||||||
|
// Configurar PNG_SHAPE en modo LOGO
|
||||||
|
auto* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
|
||||||
|
if (png_shape != nullptr) {
|
||||||
|
png_shape->setLogoMode(true);
|
||||||
|
png_shape->resetFlipCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// SALIR DEL MODO LOGO
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
void StateManager::exitLogoMode(bool return_to_demo) {
|
||||||
|
if (current_app_mode_ != AppMode::LOGO) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logo_entered_manually_ = false;
|
||||||
|
|
||||||
|
// Restaurar estado visual previo
|
||||||
|
theme_mgr_->switchToTheme(logo_previous_theme_);
|
||||||
|
engine_->setTextureByIndex(logo_previous_texture_index_);
|
||||||
|
shape_mgr_->setShapeScaleFactor(logo_previous_shape_scale_);
|
||||||
|
shape_mgr_->generateShape();
|
||||||
|
|
||||||
|
// Activar atracción física si estamos en modo SHAPE
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
scene_mgr_->enableShapeAttractionAll(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desactivar modo LOGO en PNG_SHAPE
|
||||||
|
auto* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
|
||||||
|
if (png_shape != nullptr) {
|
||||||
|
png_shape->setLogoMode(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si la figura activa es PNG_SHAPE, cambiar a otra figura aleatoria
|
||||||
|
if (shape_mgr_->getCurrentShapeType() == ShapeType::PNG_SHAPE) {
|
||||||
|
constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
||||||
|
engine_->enterShapeMode(SHAPES[rand() % 8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!return_to_demo) {
|
||||||
|
setState(AppMode::SANDBOX, 0, 0);
|
||||||
|
} else {
|
||||||
|
current_app_mode_ = previous_app_mode_;
|
||||||
|
}
|
||||||
|
}
|
||||||