Compare commits
27 Commits
f272bab296
...
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 |
@@ -16,6 +16,7 @@ Checks: >
|
||||
-performance-inefficient-string-concatenation,
|
||||
-bugprone-integer-division,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-readability-uppercase-literal-suffix,
|
||||
|
||||
WarningsAsErrors: '*'
|
||||
# Solo incluir archivos de tu código fuente
|
||||
|
||||
3
.vscode/c_cpp_properties.json
vendored
3
.vscode/c_cpp_properties.json
vendored
@@ -5,6 +5,7 @@
|
||||
"includePath": [
|
||||
"${workspaceFolder}/source",
|
||||
"${workspaceFolder}/source/external",
|
||||
"${workspaceFolder}/build/generated_shaders",
|
||||
"${env:HOMEBREW_PREFIX}/include",
|
||||
"/opt/homebrew/include"
|
||||
],
|
||||
@@ -25,6 +26,7 @@
|
||||
"includePath": [
|
||||
"${workspaceFolder}/source",
|
||||
"${workspaceFolder}/source/external",
|
||||
"${workspaceFolder}/build/generated_shaders",
|
||||
"/usr/include",
|
||||
"/usr/local/include"
|
||||
],
|
||||
@@ -41,6 +43,7 @@
|
||||
"includePath": [
|
||||
"${workspaceFolder}/source",
|
||||
"${workspaceFolder}/source/external",
|
||||
"${workspaceFolder}/build/generated_shaders",
|
||||
"C:/mingw/include"
|
||||
],
|
||||
"defines": [
|
||||
|
||||
114
CMakeLists.txt
114
CMakeLists.txt
@@ -11,86 +11,49 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Os -ffunction-sections -fdata-sec
|
||||
# Buscar SDL3 automáticamente
|
||||
find_package(SDL3 REQUIRED)
|
||||
|
||||
# Si no se encuentra SDL3, generar un error
|
||||
if (NOT SDL3_FOUND)
|
||||
message(FATAL_ERROR "SDL3 no encontrado. Por favor, verifica su instalación.")
|
||||
endif()
|
||||
|
||||
# Buscar SDL3_ttf
|
||||
find_package(SDL3_ttf REQUIRED)
|
||||
|
||||
# Si no se encuentra SDL3_ttf, generar un error
|
||||
if (NOT SDL3_ttf_FOUND)
|
||||
message(FATAL_ERROR "SDL3_ttf no encontrado. Por favor, verifica su instalación.")
|
||||
endif()
|
||||
|
||||
# ---- 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(FATAL_ERROR "glslc not found. Install the Vulkan SDK and ensure glslc is on PATH.")
|
||||
endif()
|
||||
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(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(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")
|
||||
|
||||
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_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})
|
||||
|
||||
# External runtime shaders: auto-discover and compile data/shaders/**/*.vert/*.frag
|
||||
# Output: <source>.spv alongside each source file (loaded at runtime by GpuShaderPreset)
|
||||
file(GLOB_RECURSE DATA_SHADERS
|
||||
"${CMAKE_SOURCE_DIR}/data/shaders/**/*.vert"
|
||||
"${CMAKE_SOURCE_DIR}/data/shaders/**/*.frag")
|
||||
|
||||
set(DATA_SHADER_SPVS)
|
||||
foreach(SHADER_FILE ${DATA_SHADERS})
|
||||
get_filename_component(SHADER_EXT "${SHADER_FILE}" EXT)
|
||||
if(SHADER_EXT STREQUAL ".vert")
|
||||
set(STAGE_FLAG "-fshader-stage=vertex")
|
||||
else()
|
||||
set(STAGE_FLAG "-fshader-stage=fragment")
|
||||
endif()
|
||||
set(SPV_FILE "${SHADER_FILE}.spv")
|
||||
add_custom_command(
|
||||
OUTPUT "${SPV_FILE}"
|
||||
COMMAND "${GLSLC}" ${STAGE_FLAG} -o "${SPV_FILE}" "${SHADER_FILE}"
|
||||
DEPENDS "${SHADER_FILE}"
|
||||
COMMENT "Compiling ${SHADER_FILE}"
|
||||
)
|
||||
list(APPEND DATA_SHADER_SPVS "${SPV_FILE}")
|
||||
endforeach()
|
||||
|
||||
if(DATA_SHADER_SPVS)
|
||||
add_custom_target(data_shaders ALL DEPENDS ${DATA_SHADER_SPVS})
|
||||
add_custom_target(shaders ALL DEPENDS ${SPIRV_HEADERS})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -98,6 +61,9 @@ endif()
|
||||
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")
|
||||
|
||||
# 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
|
||||
if(NOT SOURCE_FILES)
|
||||
message(FATAL_ERROR "No se encontraron archivos fuente en el directorio 'source/'. Verifica la ruta.")
|
||||
@@ -105,13 +71,10 @@ endif()
|
||||
|
||||
# Detectar la plataforma y configuraciones específicas
|
||||
if(WIN32)
|
||||
set(PLATFORM windows)
|
||||
set(LINK_LIBS SDL3::SDL3 SDL3_ttf::SDL3_ttf mingw32 ws2_32)
|
||||
elseif(UNIX AND NOT APPLE)
|
||||
set(PLATFORM linux)
|
||||
set(LINK_LIBS SDL3::SDL3 SDL3_ttf::SDL3_ttf)
|
||||
elseif(APPLE)
|
||||
set(PLATFORM macos)
|
||||
set(LINK_LIBS SDL3::SDL3 SDL3_ttf::SDL3_ttf)
|
||||
endif()
|
||||
|
||||
@@ -131,11 +94,10 @@ set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAK
|
||||
target_link_libraries(${PROJECT_NAME} ${LINK_LIBS})
|
||||
|
||||
if(NOT APPLE)
|
||||
add_dependencies(${PROJECT_NAME} shaders)
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE "${SHADER_GEN_DIR}")
|
||||
if(TARGET data_shaders)
|
||||
add_dependencies(${PROJECT_NAME} data_shaders)
|
||||
if(GLSLC)
|
||||
add_dependencies(${PROJECT_NAME} shaders)
|
||||
endif()
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE "${SHADER_GEN_DIR}")
|
||||
endif()
|
||||
|
||||
# Tool: pack_resources
|
||||
|
||||
67
Makefile
67
Makefile
@@ -2,16 +2,15 @@
|
||||
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
|
||||
DIR_SOURCES := $(addsuffix /, $(DIR_ROOT)source)
|
||||
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
|
||||
DIR_BUILD := $(addsuffix /, $(DIR_ROOT)build)
|
||||
DIR_TOOLS := $(addsuffix /, $(DIR_ROOT)tools)
|
||||
|
||||
# Variables
|
||||
TARGET_NAME := vibe3_physics
|
||||
TARGET_FILE := $(DIR_BIN)$(TARGET_NAME)
|
||||
APP_NAME := ViBe3 Physics
|
||||
RELEASE_FOLDER := vibe3_release
|
||||
RELEASE_FOLDER := dist/_tmp
|
||||
RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
|
||||
RESOURCE_FILE := release/windows/vibe3.res
|
||||
RESOURCE_FILE := build/vibe3.res
|
||||
DIST_DIR := dist
|
||||
|
||||
# Variables para herramienta de empaquetado
|
||||
@@ -51,6 +50,7 @@ RASPI_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-raspberry.t
|
||||
# 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) \
|
||||
@@ -64,8 +64,18 @@ APP_SOURCES := $(wildcard source/*.cpp) \
|
||||
# Excluir archivos antiguos si existen
|
||||
APP_SOURCES := $(filter-out source/main_old.cpp, $(APP_SOURCES))
|
||||
|
||||
# Includes
|
||||
INCLUDES := -Isource -Isource/external
|
||||
# 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
|
||||
@@ -100,13 +110,17 @@ endif
|
||||
# Reglas para herramienta de empaquetado y resources.pack
|
||||
$(PACK_TOOL): $(PACK_SOURCES)
|
||||
@echo "Compilando herramienta de empaquetado..."
|
||||
$(PACK_CXX) -std=c++17 -Wall -Os $(PACK_INCLUDES) $(PACK_SOURCES) -o $(PACK_TOOL)
|
||||
$(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)
|
||||
DATA_FILES := $(shell find data -type f 2>/dev/null)
|
||||
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/..."
|
||||
@@ -122,8 +136,7 @@ force_resource_pack: $(PACK_TOOL)
|
||||
|
||||
# Reglas para compilación
|
||||
windows:
|
||||
@echo off
|
||||
@echo Compilando para Windows con nombre: "$(APP_NAME).exe"
|
||||
@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
|
||||
@@ -133,6 +146,7 @@ windows_release: force_resource_pack
|
||||
|
||||
# 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'
|
||||
@@ -144,7 +158,6 @@ windows_release: force_resource_pack
|
||||
@copy /Y release\windows\dll\*.dll "$(RELEASE_FOLDER)\" >nul 2>&1 || echo DLLs copied successfully
|
||||
|
||||
# Compila
|
||||
@if not exist "$(DIST_DIR)" mkdir "$(DIST_DIR)"
|
||||
@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
|
||||
@@ -197,40 +210,6 @@ macos_release: force_resource_pack
|
||||
cp LICENSE "$(RELEASE_FOLDER)"
|
||||
cp README.md "$(RELEASE_FOLDER)"
|
||||
|
||||
# NOTA: create-dmg crea automáticamente el enlace a /Applications con --app-drop-link
|
||||
# No es necesario crearlo manualmente aquí
|
||||
|
||||
# Compila la versión para procesadores Intel
|
||||
ifdef ENABLE_MACOS_X86_64
|
||||
$(CXX) $(APP_SOURCES) $(INCLUDES) -DMACOS_BUNDLE $(CXXFLAGS) $(LDFLAGS) -o "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" -rpath @executable_path/../Frameworks/ -target x86_64-apple-macos12
|
||||
|
||||
# Firma la aplicación
|
||||
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
|
||||
|
||||
# Empaqueta el .dmg de la versión Intel con create-dmg
|
||||
@echo "Creando DMG Intel 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_INTEL_RELEASE)" \
|
||||
"$(RELEASE_FOLDER)"
|
||||
@if [ -f "$(MACOS_INTEL_RELEASE)" ]; then \
|
||||
echo "✓ Release Intel creado exitosamente: $(MACOS_INTEL_RELEASE)"; \
|
||||
else \
|
||||
echo "✗ Error: No se pudo crear el DMG Intel"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@rm -f rw.*.dmg 2>/dev/null || true
|
||||
endif
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
87
README.md
87
README.md
@@ -531,49 +531,92 @@ vibe3_physics/
|
||||
## 🔧 Requisitos del Sistema
|
||||
|
||||
- **SDL3** (Simple DirectMedia Layer 3)
|
||||
- **SDL3_ttf** (fuentes TTF para SDL3)
|
||||
- **C++20** compatible compiler (GCC 10+, Clang 12+, MSVC 2019+)
|
||||
- **CMake 3.20+** o **Make**
|
||||
- **CMake 3.20+**
|
||||
- **Plataforma**: Windows, Linux, macOS
|
||||
|
||||
### Instalación de SDL3
|
||||
### Dependencias de shaders
|
||||
|
||||
El proyecto compila shaders GLSL a SPIR-V en todas las plataformas usando `glslc` (incluido en el Vulkan SDK). En macOS, adicionalmente transpila los `.spv` a MSL (Metal) con `spirv-cross` (shaders internos: sprite, ball, postfx).
|
||||
|
||||
| Plataforma | Backend GPU | Herramienta de shaders | Resultado |
|
||||
|------------|-------------|------------------------|-----------|
|
||||
| Linux | Vulkan | `glslc` (Vulkan SDK) | `.spv` |
|
||||
| Windows | Vulkan | `glslc` (Vulkan SDK) | `.spv` |
|
||||
| macOS | Metal | `glslc` + `spirv-cross`| `.spv.msl`|
|
||||
|
||||
---
|
||||
|
||||
### Instalación de dependencias
|
||||
|
||||
#### macOS
|
||||
|
||||
#### Windows (MinGW)
|
||||
```bash
|
||||
# Usando vcpkg
|
||||
vcpkg install sdl3
|
||||
# SDL3 + SDL3_ttf
|
||||
brew install sdl3 sdl3_ttf
|
||||
|
||||
# O compilar desde fuente
|
||||
git clone https://github.com/libsdl-org/SDL
|
||||
cd SDL
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
sudo make install
|
||||
# Vulkan SDK (incluye glslc para compilar shaders)
|
||||
# Descargar desde https://vulkan.lunarg.com/ o via Homebrew:
|
||||
brew install --cask vulkan-sdk
|
||||
|
||||
# spirv-cross (transpilación SPIR-V → MSL para Metal)
|
||||
brew install spirv-cross
|
||||
```
|
||||
|
||||
Tras instalar el Vulkan SDK es posible que necesites añadir `glslc` al PATH:
|
||||
```bash
|
||||
# Añadir a ~/.zshrc o ~/.bash_profile (ajusta la versión):
|
||||
export PATH="$HOME/VulkanSDK/<version>/macOS/bin:$PATH"
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
#### Linux (Ubuntu/Debian)
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install libsdl3-dev
|
||||
|
||||
# Si no está disponible, compilar desde fuente
|
||||
```bash
|
||||
# SDL3 + SDL3_ttf
|
||||
sudo apt update
|
||||
sudo apt install libsdl3-dev libsdl3-ttf-dev
|
||||
|
||||
# Vulkan SDK (incluye glslc)
|
||||
# Opción A: paquete del sistema
|
||||
sudo apt install glslc
|
||||
|
||||
# Opción B: Vulkan SDK completo (recomendado para versión reciente)
|
||||
wget -qO- https://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add -
|
||||
sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan.list \
|
||||
https://packages.lunarg.com/vulkan/lunarg-vulkan-focal.list
|
||||
sudo apt update
|
||||
sudo apt install vulkan-sdk
|
||||
```
|
||||
|
||||
#### Linux (Arch)
|
||||
|
||||
```bash
|
||||
sudo pacman -S sdl3
|
||||
sudo pacman -S sdl3 sdl3_ttf vulkan-devel glslang shaderc
|
||||
```
|
||||
|
||||
#### macOS
|
||||
#### Windows (MinGW / MSYS2)
|
||||
|
||||
```bash
|
||||
brew install sdl3
|
||||
# En MSYS2 shell:
|
||||
pacman -S mingw-w64-x86_64-SDL3 mingw-w64-x86_64-SDL3_ttf
|
||||
|
||||
# Vulkan SDK: descargar el instalador desde https://vulkan.lunarg.com/
|
||||
# El instalador añade glslc al PATH automáticamente.
|
||||
```
|
||||
|
||||
Con Visual Studio + vcpkg:
|
||||
```bash
|
||||
vcpkg install sdl3 sdl3-ttf
|
||||
# Instalar Vulkan SDK desde https://vulkan.lunarg.com/ (incluye glslc)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Compilación
|
||||
|
||||
### Opción 1: CMake (Recomendado)
|
||||
### CMake (todas las plataformas)
|
||||
|
||||
```bash
|
||||
mkdir -p build && cd build
|
||||
@@ -581,7 +624,9 @@ cmake ..
|
||||
cmake --build .
|
||||
```
|
||||
|
||||
### Opción 2: Make directo
|
||||
En macOS, CMake detecta automáticamente `spirv-cross` y genera los `.spv.msl` necesarios para los shaders internos (sprite, ball, postfx).
|
||||
|
||||
### Make directo (Linux/macOS)
|
||||
|
||||
```bash
|
||||
make
|
||||
|
||||
Binary file not shown.
BIN
data/fonts/Exo2-Regular.ttf
Normal file
BIN
data/fonts/Exo2-Regular.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,28 +0,0 @@
|
||||
# Based on dannyld's rainbow settings
|
||||
|
||||
shaders = 2
|
||||
|
||||
shader0 = "../crt/shaders/mame_hlsl/shaders/mame_ntsc_encode.slang"
|
||||
filter_linear0 = "true"
|
||||
scale_type0 = "source"
|
||||
scale0 = "1.000000"
|
||||
|
||||
shader1 = "../crt/shaders/mame_hlsl/shaders/mame_ntsc_decode.slang"
|
||||
filter_linear1 = "true"
|
||||
scale_type1 = "source"
|
||||
scale_1 = "1.000000"
|
||||
|
||||
# ntsc parameters
|
||||
ntscsignal = "1.000000"
|
||||
avalue = "0.000000"
|
||||
bvalue = "0.000000"
|
||||
scantime = "47.900070"
|
||||
|
||||
# optional blur
|
||||
shadowalpha = "0.100000"
|
||||
notch_width = "3.450001"
|
||||
ifreqresponse = "1.750000"
|
||||
qfreqresponse = "1.450000"
|
||||
|
||||
# uncomment for jailbars in blue
|
||||
#pvalue = "1.100000"
|
||||
@@ -1,69 +0,0 @@
|
||||
#version 450
|
||||
// license:BSD-3-Clause
|
||||
// copyright-holders:Ryan Holtz,ImJezze
|
||||
// Adapted from mame_ntsc_encode.slang for SDL3 GPU / Vulkan SPIRV
|
||||
|
||||
layout(location=0) in vec2 v_uv;
|
||||
layout(location=0) out vec4 FragColor;
|
||||
|
||||
layout(set=2, binding=0) uniform sampler2D Source;
|
||||
|
||||
layout(set=3, binding=0) uniform NTSCParams {
|
||||
float source_width;
|
||||
float source_height;
|
||||
float a_value;
|
||||
float b_value;
|
||||
float cc_value;
|
||||
float scan_time;
|
||||
float notch_width;
|
||||
float y_freq;
|
||||
float i_freq;
|
||||
float q_freq;
|
||||
float _pad0;
|
||||
float _pad1;
|
||||
} u;
|
||||
|
||||
const float PI = 3.1415927;
|
||||
const float PI2 = PI * 2.0;
|
||||
|
||||
void main() {
|
||||
vec2 source_dims = vec2(u.source_width, u.source_height);
|
||||
|
||||
// p_value=1: one texel step per sub-sample (no horizontal stretch)
|
||||
vec2 PValueSourceTexel = vec2(1.0, 0.0) / source_dims;
|
||||
|
||||
vec2 C0 = v_uv + PValueSourceTexel * vec2(0.00, 0.0);
|
||||
vec2 C1 = v_uv + PValueSourceTexel * vec2(0.25, 0.0);
|
||||
vec2 C2 = v_uv + PValueSourceTexel * vec2(0.50, 0.0);
|
||||
vec2 C3 = v_uv + PValueSourceTexel * vec2(0.75, 0.0);
|
||||
|
||||
vec4 Cx = vec4(C0.x, C1.x, C2.x, C3.x);
|
||||
vec4 Cy = vec4(C0.y, C1.y, C2.y, C3.y);
|
||||
|
||||
vec4 Texel0 = texture(Source, C0);
|
||||
vec4 Texel1 = texture(Source, C1);
|
||||
vec4 Texel2 = texture(Source, C2);
|
||||
vec4 Texel3 = texture(Source, C3);
|
||||
|
||||
vec4 HPosition = Cx;
|
||||
vec4 VPosition = Cy;
|
||||
|
||||
const vec4 YDot = vec4(0.299, 0.587, 0.114, 0.0);
|
||||
const vec4 IDot = vec4(0.595716, -0.274453, -0.321263, 0.0);
|
||||
const vec4 QDot = vec4(0.211456, -0.522591, 0.311135, 0.0);
|
||||
|
||||
vec4 Y = vec4(dot(Texel0, YDot), dot(Texel1, YDot), dot(Texel2, YDot), dot(Texel3, YDot));
|
||||
vec4 I = vec4(dot(Texel0, IDot), dot(Texel1, IDot), dot(Texel2, IDot), dot(Texel3, IDot));
|
||||
vec4 Q = vec4(dot(Texel0, QDot), dot(Texel1, QDot), dot(Texel2, QDot), dot(Texel3, QDot));
|
||||
|
||||
float W = PI2 * u.cc_value * u.scan_time;
|
||||
float WoPI = W / PI;
|
||||
|
||||
float HOffset = u.a_value / WoPI;
|
||||
float VScale = u.b_value * source_dims.y / WoPI;
|
||||
|
||||
vec4 T = HPosition + vec4(HOffset) + VPosition * vec4(VScale);
|
||||
vec4 TW = T * W;
|
||||
|
||||
FragColor = Y + I * cos(TW) + Q * sin(TW);
|
||||
}
|
||||
Binary file not shown.
@@ -1,8 +0,0 @@
|
||||
#version 450
|
||||
layout(location=0) out vec2 v_uv;
|
||||
void main() {
|
||||
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];
|
||||
}
|
||||
Binary file not shown.
@@ -1,148 +0,0 @@
|
||||
#version 450
|
||||
// license:BSD-3-Clause
|
||||
// copyright-holders:Ryan Holtz,ImJezze
|
||||
// Adapted from mame_ntsc_decode.slang for SDL3 GPU / Vulkan SPIRV
|
||||
|
||||
layout(location=0) in vec2 v_uv;
|
||||
layout(location=0) out vec4 FragColor;
|
||||
|
||||
layout(set=2, binding=0) uniform sampler2D Source;
|
||||
|
||||
layout(set=3, binding=0) uniform NTSCParams {
|
||||
float source_width;
|
||||
float source_height;
|
||||
float a_value;
|
||||
float b_value;
|
||||
float cc_value;
|
||||
float scan_time;
|
||||
float notch_width;
|
||||
float y_freq;
|
||||
float i_freq;
|
||||
float q_freq;
|
||||
float _pad0;
|
||||
float _pad1;
|
||||
} u;
|
||||
|
||||
const float PI = 3.1415927;
|
||||
const float PI2 = PI * 2.0;
|
||||
|
||||
const vec3 RDot = vec3(1.0, 0.956, 0.621);
|
||||
const vec3 GDot = vec3(1.0, -0.272, -0.647);
|
||||
const vec3 BDot = vec3(1.0, -1.106, 1.703);
|
||||
|
||||
const vec4 NotchOffset = vec4(0.0, 1.0, 2.0, 3.0);
|
||||
const int SampleCount = 64;
|
||||
const int HalfSampleCount = 32;
|
||||
|
||||
void main() {
|
||||
vec2 source_dims = vec2(u.source_width, u.source_height);
|
||||
vec4 BaseTexel = texture(Source, v_uv);
|
||||
|
||||
float CCValue = u.cc_value;
|
||||
float ScanTime = u.scan_time;
|
||||
float NotchHalfWidth = u.notch_width / 2.0;
|
||||
float YFreqResponse = u.y_freq;
|
||||
float IFreqResponse = u.i_freq;
|
||||
float QFreqResponse = u.q_freq;
|
||||
float AValue = u.a_value;
|
||||
float BValue = u.b_value;
|
||||
|
||||
float TimePerSample = ScanTime / (source_dims.x * 4.0);
|
||||
|
||||
float Fc_y1 = (CCValue - NotchHalfWidth) * TimePerSample;
|
||||
float Fc_y2 = (CCValue + NotchHalfWidth) * TimePerSample;
|
||||
float Fc_y3 = YFreqResponse * TimePerSample;
|
||||
float Fc_i = IFreqResponse * TimePerSample;
|
||||
float Fc_q = QFreqResponse * TimePerSample;
|
||||
|
||||
float Fc_i_2 = Fc_i * 2.0;
|
||||
float Fc_q_2 = Fc_q * 2.0;
|
||||
float Fc_y1_2 = Fc_y1 * 2.0;
|
||||
float Fc_y2_2 = Fc_y2 * 2.0;
|
||||
float Fc_y3_2 = Fc_y3 * 2.0;
|
||||
float Fc_i_pi2 = Fc_i * PI2;
|
||||
float Fc_q_pi2 = Fc_q * PI2;
|
||||
float Fc_y1_pi2 = Fc_y1 * PI2;
|
||||
float Fc_y2_pi2 = Fc_y2 * PI2;
|
||||
float Fc_y3_pi2 = Fc_y3 * PI2;
|
||||
float PI2Length = PI2 / float(SampleCount);
|
||||
|
||||
float W = PI2 * CCValue * ScanTime;
|
||||
float WoPI = W / PI;
|
||||
|
||||
float HOffset = BValue / WoPI;
|
||||
float VScale = AValue * source_dims.y / WoPI;
|
||||
|
||||
vec4 YAccum = vec4(0.0);
|
||||
vec4 IAccum = vec4(0.0);
|
||||
vec4 QAccum = vec4(0.0);
|
||||
|
||||
vec4 Cy = vec4(v_uv.y);
|
||||
vec4 VPosition = Cy;
|
||||
|
||||
for (float i = 0.0; i < float(SampleCount); i += 4.0) {
|
||||
float n = i - float(HalfSampleCount);
|
||||
vec4 n4 = n + NotchOffset;
|
||||
|
||||
vec4 Cx = vec4(v_uv.x) + (n4 * 0.25) / source_dims.x;
|
||||
vec4 HPosition = Cx;
|
||||
|
||||
vec4 C = texture(Source, vec2(Cx.r, Cy.r));
|
||||
vec4 T = HPosition + vec4(HOffset) + VPosition * vec4(VScale);
|
||||
vec4 WT = W * T;
|
||||
|
||||
vec4 SincKernel = 0.54 + 0.46 * cos(PI2Length * n4);
|
||||
|
||||
vec4 SincYIn1 = Fc_y1_pi2 * n4;
|
||||
vec4 SincYIn2 = Fc_y2_pi2 * n4;
|
||||
vec4 SincYIn3 = Fc_y3_pi2 * n4;
|
||||
vec4 SincIIn = Fc_i_pi2 * n4;
|
||||
vec4 SincQIn = Fc_q_pi2 * n4;
|
||||
|
||||
vec4 SincY1, SincY2, SincY3;
|
||||
SincY1.x = (SincYIn1.x != 0.0) ? sin(SincYIn1.x) / SincYIn1.x : 1.0;
|
||||
SincY1.y = (SincYIn1.y != 0.0) ? sin(SincYIn1.y) / SincYIn1.y : 1.0;
|
||||
SincY1.z = (SincYIn1.z != 0.0) ? sin(SincYIn1.z) / SincYIn1.z : 1.0;
|
||||
SincY1.w = (SincYIn1.w != 0.0) ? sin(SincYIn1.w) / SincYIn1.w : 1.0;
|
||||
SincY2.x = (SincYIn2.x != 0.0) ? sin(SincYIn2.x) / SincYIn2.x : 1.0;
|
||||
SincY2.y = (SincYIn2.y != 0.0) ? sin(SincYIn2.y) / SincYIn2.y : 1.0;
|
||||
SincY2.z = (SincYIn2.z != 0.0) ? sin(SincYIn2.z) / SincYIn2.z : 1.0;
|
||||
SincY2.w = (SincYIn2.w != 0.0) ? sin(SincYIn2.w) / SincYIn2.w : 1.0;
|
||||
SincY3.x = (SincYIn3.x != 0.0) ? sin(SincYIn3.x) / SincYIn3.x : 1.0;
|
||||
SincY3.y = (SincYIn3.y != 0.0) ? sin(SincYIn3.y) / SincYIn3.y : 1.0;
|
||||
SincY3.z = (SincYIn3.z != 0.0) ? sin(SincYIn3.z) / SincYIn3.z : 1.0;
|
||||
SincY3.w = (SincYIn3.w != 0.0) ? sin(SincYIn3.w) / SincYIn3.w : 1.0;
|
||||
|
||||
vec4 IdealY = Fc_y1_2 * SincY1 - Fc_y2_2 * SincY2 + Fc_y3_2 * SincY3;
|
||||
|
||||
vec4 IdealI, IdealQ;
|
||||
IdealI.x = Fc_i_2 * ((SincIIn.x != 0.0) ? sin(SincIIn.x) / SincIIn.x : 1.0);
|
||||
IdealI.y = Fc_i_2 * ((SincIIn.y != 0.0) ? sin(SincIIn.y) / SincIIn.y : 1.0);
|
||||
IdealI.z = Fc_i_2 * ((SincIIn.z != 0.0) ? sin(SincIIn.z) / SincIIn.z : 1.0);
|
||||
IdealI.w = Fc_i_2 * ((SincIIn.w != 0.0) ? sin(SincIIn.w) / SincIIn.w : 1.0);
|
||||
IdealQ.x = Fc_q_2 * ((SincQIn.x != 0.0) ? sin(SincQIn.x) / SincQIn.x : 1.0);
|
||||
IdealQ.y = Fc_q_2 * ((SincQIn.y != 0.0) ? sin(SincQIn.y) / SincQIn.y : 1.0);
|
||||
IdealQ.z = Fc_q_2 * ((SincQIn.z != 0.0) ? sin(SincQIn.z) / SincQIn.z : 1.0);
|
||||
IdealQ.w = Fc_q_2 * ((SincQIn.w != 0.0) ? sin(SincQIn.w) / SincQIn.w : 1.0);
|
||||
|
||||
vec4 FilterY = SincKernel * IdealY;
|
||||
vec4 FilterI = SincKernel * IdealI;
|
||||
vec4 FilterQ = SincKernel * IdealQ;
|
||||
|
||||
YAccum += C * FilterY;
|
||||
IAccum += C * cos(WT) * FilterI;
|
||||
QAccum += C * sin(WT) * FilterQ;
|
||||
}
|
||||
|
||||
vec3 YIQ = vec3(
|
||||
(YAccum.r + YAccum.g + YAccum.b + YAccum.a),
|
||||
(IAccum.r + IAccum.g + IAccum.b + IAccum.a) * 2.0,
|
||||
(QAccum.r + QAccum.g + QAccum.b + QAccum.a) * 2.0);
|
||||
|
||||
vec3 RGB = vec3(
|
||||
dot(YIQ, RDot),
|
||||
dot(YIQ, GDot),
|
||||
dot(YIQ, BDot));
|
||||
|
||||
FragColor = vec4(RGB, BaseTexel.a);
|
||||
}
|
||||
Binary file not shown.
@@ -1,8 +0,0 @@
|
||||
#version 450
|
||||
layout(location=0) out vec2 v_uv;
|
||||
void main() {
|
||||
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];
|
||||
}
|
||||
Binary file not shown.
@@ -1,18 +0,0 @@
|
||||
name = ntsc-md-rainbows
|
||||
passes = 2
|
||||
|
||||
pass0_vert = pass0_encode.vert
|
||||
pass0_frag = pass0_encode.frag
|
||||
|
||||
pass1_vert = pass1_decode.vert
|
||||
pass1_frag = pass1_decode.frag
|
||||
|
||||
; NTSC parameters (mapped to NTSCParams uniform buffer)
|
||||
scantime = 47.9
|
||||
avalue = 0.0
|
||||
bvalue = 0.0
|
||||
ccvalue = 3.5795455
|
||||
notch_width = 3.45
|
||||
yfreqresponse = 1.75
|
||||
ifreqresponse = 1.75
|
||||
qfreqresponse = 1.45
|
||||
@@ -1,2 +1,2 @@
|
||||
// coffee.rc
|
||||
IDI_ICON1 ICON "icon.ico"
|
||||
IDI_ICON1 ICON "release/icons/icon.ico"
|
||||
|
||||
@@ -6,7 +6,7 @@ layout(set=3, binding=0) uniform PostFXUniforms {
|
||||
float vignette_strength;
|
||||
float chroma_strength;
|
||||
float scanline_strength;
|
||||
float time;
|
||||
float screen_height;
|
||||
} u;
|
||||
void main() {
|
||||
float ca = u.chroma_strength * 0.005;
|
||||
@@ -15,7 +15,7 @@ void main() {
|
||||
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 * 720.0);
|
||||
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;
|
||||
|
||||
5
shaders/precompiled/ball_vert_spv.h
Normal file
5
shaders/precompiled/ball_vert_spv.h
Normal file
File diff suppressed because one or more lines are too long
5
shaders/precompiled/postfx_frag_spv.h
Normal file
5
shaders/precompiled/postfx_frag_spv.h
Normal file
File diff suppressed because one or more lines are too long
5
shaders/precompiled/postfx_vert_spv.h
Normal file
5
shaders/precompiled/postfx_vert_spv.h
Normal file
File diff suppressed because one or more lines are too long
5
shaders/precompiled/sprite_frag_spv.h
Normal file
5
shaders/precompiled/sprite_frag_spv.h
Normal file
File diff suppressed because one or more lines are too long
5
shaders/precompiled/sprite_vert_spv.h
Normal file
5
shaders/precompiled/sprite_vert_spv.h
Normal file
File diff suppressed because one or more lines are too long
@@ -1,30 +1,31 @@
|
||||
#include "ball.hpp"
|
||||
|
||||
#include <stdlib.h> // for rand
|
||||
|
||||
#include <cmath> // for fabs
|
||||
#include <algorithm>
|
||||
#include <cmath> // for fabs
|
||||
#include <cstdlib> // for rand
|
||||
#include <utility>
|
||||
|
||||
#include "defines.hpp" // for Color, SCREEN_HEIGHT, GRAVITY_FORCE
|
||||
class Texture;
|
||||
|
||||
// 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)
|
||||
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)
|
||||
}
|
||||
|
||||
// Función auxiliar para generar pérdida lateral aleatoria
|
||||
float generateLateralLoss() {
|
||||
auto generateLateralLoss() -> float {
|
||||
// Genera un valor entre 0 y 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)
|
||||
}
|
||||
|
||||
// Constructor
|
||||
Ball::Ball(float x, float y, float vx, float vy, Color color, std::shared_ptr<Texture> texture, int screen_width, int screen_height, int ball_size, 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)),
|
||||
pos_({x, y, static_cast<float>(ball_size), static_cast<float>(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)
|
||||
vx_ = vx * 60.0f;
|
||||
vy_ = vy * 60.0f;
|
||||
@@ -36,7 +37,7 @@ Ball::Ball(float x, float y, float vx, float vy, Color color, std::shared_ptr<Te
|
||||
gravity_force_ = GRAVITY_FORCE * 60.0f * 60.0f;
|
||||
gravity_mass_factor_ = mass_factor; // Factor de masa individual para esta pelota
|
||||
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;
|
||||
on_surface_ = false;
|
||||
// Coeficiente base IGUAL para todas las pelotas (solo variación por rebote individual)
|
||||
@@ -54,11 +55,11 @@ Ball::Ball(float x, float y, float vx, float vy, Color color, std::shared_ptr<Te
|
||||
}
|
||||
|
||||
// Actualiza la lógica de la clase
|
||||
void Ball::update(float deltaTime) {
|
||||
void Ball::update(float delta_time) { // NOLINT(readability-function-cognitive-complexity)
|
||||
// Aplica la gravedad según la dirección (píxeles/segundo²)
|
||||
if (!on_surface_) {
|
||||
// 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_) {
|
||||
case GravityDirection::DOWN:
|
||||
vy_ += effective_gravity;
|
||||
@@ -77,26 +78,26 @@ void Ball::update(float deltaTime) {
|
||||
|
||||
// Actualiza la posición en función de la velocidad (píxeles/segundo)
|
||||
if (!on_surface_) {
|
||||
pos_.x += vx_ * deltaTime;
|
||||
pos_.y += vy_ * deltaTime;
|
||||
pos_.x += vx_ * delta_time;
|
||||
pos_.y += vy_ * delta_time;
|
||||
} else {
|
||||
// Si está en superficie, mantener posición según dirección de gravedad
|
||||
switch (gravity_direction_) {
|
||||
case GravityDirection::DOWN:
|
||||
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;
|
||||
case GravityDirection::UP:
|
||||
pos_.y = 0;
|
||||
pos_.x += vx_ * deltaTime; // Seguir moviéndose en X
|
||||
pos_.x += vx_ * delta_time; // Seguir moviéndose en X
|
||||
break;
|
||||
case GravityDirection::LEFT:
|
||||
pos_.x = 0;
|
||||
pos_.y += vy_ * deltaTime; // Seguir moviéndose en Y
|
||||
pos_.y += vy_ * delta_time; // Seguir moviéndose en Y
|
||||
break;
|
||||
case GravityDirection::RIGHT:
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -176,7 +177,7 @@ void Ball::update(float deltaTime) {
|
||||
// Aplica rozamiento al estar en superficie
|
||||
if (on_surface_) {
|
||||
// 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_) {
|
||||
case GravityDirection::DOWN:
|
||||
@@ -246,7 +247,7 @@ void Ball::setGravityDirection(GravityDirection direction) {
|
||||
// Aplica un pequeño empuje lateral aleatorio
|
||||
void Ball::applyRandomLateralPush() {
|
||||
// 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 -)
|
||||
int sign = ((rand() % 2) * 2) - 1;
|
||||
@@ -304,18 +305,18 @@ void Ball::enableShapeAttraction(bool enable) {
|
||||
}
|
||||
|
||||
// Obtener distancia actual al punto objetivo (para calcular convergencia)
|
||||
float Ball::getDistanceToTarget() const {
|
||||
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);
|
||||
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 deltaTime,
|
||||
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;
|
||||
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
|
||||
@@ -334,7 +335,7 @@ void Ball::applyShapeForce(float target_x, float target_y, float sphere_radius,
|
||||
float diff_y = target_y - pos_.y;
|
||||
|
||||
// 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)
|
||||
float spring_force_x = spring_k * diff_x;
|
||||
@@ -354,7 +355,7 @@ void Ball::applyShapeForce(float target_x, float target_y, float sphere_radius,
|
||||
float total_force_y = spring_force_y - damping_force_y;
|
||||
|
||||
// Limitar magnitud de fuerza (evitar explosiones numéricas)
|
||||
float force_magnitude = sqrtf(total_force_x * total_force_x + total_force_y * total_force_y);
|
||||
float force_magnitude = sqrtf((total_force_x * total_force_x) + (total_force_y * total_force_y));
|
||||
if (force_magnitude > max_force) {
|
||||
float scale_limit = max_force / force_magnitude;
|
||||
total_force_x *= scale_limit;
|
||||
@@ -363,18 +364,22 @@ void Ball::applyShapeForce(float target_x, float target_y, float sphere_radius,
|
||||
|
||||
// Aplicar aceleración (F = ma, asumiendo m = 1 para simplificar)
|
||||
// a = F/m, pero m=1, así que a = F
|
||||
vx_ += total_force_x * deltaTime;
|
||||
vy_ += total_force_y * deltaTime;
|
||||
vx_ += total_force_x * delta_time;
|
||||
vy_ += total_force_y * delta_time;
|
||||
|
||||
// Actualizar posición con física normal (velocidad integrada)
|
||||
pos_.x += vx_ * deltaTime;
|
||||
pos_.y += vy_ * deltaTime;
|
||||
pos_.x += vx_ * delta_time;
|
||||
pos_.y += vy_ * delta_time;
|
||||
|
||||
// Mantener pelotas dentro de los límites de pantalla
|
||||
if (pos_.x < 0) pos_.x = 0;
|
||||
if (pos_.x + pos_.w > screen_width_) pos_.x = screen_width_ - pos_.w;
|
||||
if (pos_.y < 0) pos_.y = 0;
|
||||
if (pos_.y + pos_.h > screen_height_) pos_.y = screen_height_ - pos_.h;
|
||||
pos_.x = std::max<float>(pos_.x, 0);
|
||||
if (pos_.x + pos_.w > screen_width_) {
|
||||
pos_.x = screen_width_ - pos_.w;
|
||||
}
|
||||
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
|
||||
sprite_->setPos({pos_.x, pos_.y});
|
||||
@@ -393,5 +398,5 @@ void Ball::updateSize(int new_size) {
|
||||
|
||||
void Ball::setTexture(std::shared_ptr<Texture> texture) {
|
||||
// Actualizar textura del sprite
|
||||
sprite_->setTexture(texture);
|
||||
sprite_->setTexture(std::move(texture));
|
||||
}
|
||||
@@ -10,34 +10,34 @@ 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
|
||||
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?
|
||||
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, std::shared_ptr<Texture> texture, int screen_width, int screen_height, int ball_size, GravityDirection gravity_dir = GravityDirection::DOWN, float mass_factor = 1.0f);
|
||||
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 deltaTime);
|
||||
void update(float delta_time);
|
||||
|
||||
// Pinta la clase
|
||||
void render();
|
||||
@@ -72,11 +72,20 @@ class Ball {
|
||||
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; }
|
||||
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; }
|
||||
void setPosition(float x, float y) {
|
||||
pos_.x = x;
|
||||
pos_.y = y;
|
||||
}
|
||||
|
||||
// Getters/Setters para batch rendering
|
||||
SDL_FRect getPosition() const { return pos_; }
|
||||
@@ -84,7 +93,7 @@ class Ball {
|
||||
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 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)
|
||||
@@ -99,10 +108,5 @@ class Ball {
|
||||
// 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 deltaTime,
|
||||
float spring_k = SHAPE_SPRING_K,
|
||||
float damping_base = SHAPE_DAMPING_BASE,
|
||||
float damping_near = SHAPE_DAMPING_NEAR,
|
||||
float near_threshold = SHAPE_NEAR_THRESHOLD,
|
||||
float max_force = SHAPE_MAX_FORCE);
|
||||
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);
|
||||
};
|
||||
@@ -3,39 +3,38 @@
|
||||
#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
|
||||
#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) {
|
||||
: 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() {
|
||||
}
|
||||
BoidManager::~BoidManager() = default;
|
||||
|
||||
void BoidManager::initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr,
|
||||
StateManager* state_mgr, int screen_width, int screen_height) {
|
||||
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;
|
||||
@@ -65,7 +64,8 @@ void BoidManager::activateBoids() {
|
||||
auto& balls = scene_mgr_->getBallsMutable();
|
||||
for (auto& ball : balls) {
|
||||
// Dar velocidad inicial aleatoria si está quieto
|
||||
float vx, vy;
|
||||
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)
|
||||
@@ -76,13 +76,15 @@ void BoidManager::activateBoids() {
|
||||
}
|
||||
|
||||
// Mostrar notificación (solo si NO estamos en modo demo o logo)
|
||||
if (state_mgr_ && ui_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||
ui_mgr_->showNotification("Modo Boids");
|
||||
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;
|
||||
if (!boids_active_) {
|
||||
return;
|
||||
}
|
||||
|
||||
boids_active_ = false;
|
||||
|
||||
@@ -92,8 +94,8 @@ void BoidManager::deactivateBoids(bool force_gravity_on) {
|
||||
}
|
||||
|
||||
// Mostrar notificación (solo si NO estamos en modo demo o logo)
|
||||
if (state_mgr_ && ui_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||
ui_mgr_->showNotification("Modo Física");
|
||||
if ((state_mgr_ != nullptr) && (ui_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||
ui_mgr_->showNotification("Modo física");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +108,9 @@ void BoidManager::toggleBoidsMode(bool force_gravity_on) {
|
||||
}
|
||||
|
||||
void BoidManager::update(float delta_time) {
|
||||
if (!boids_active_) return;
|
||||
if (!boids_active_) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& balls = scene_mgr_->getBallsMutable();
|
||||
|
||||
@@ -114,8 +118,8 @@ void BoidManager::update(float delta_time) {
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -131,7 +135,8 @@ void BoidManager::update(float delta_time) {
|
||||
|
||||
// Actualizar posiciones con velocidades resultantes (time-based)
|
||||
for (auto& ball : balls) {
|
||||
float vx, vy;
|
||||
float vx;
|
||||
float vy;
|
||||
ball->getVelocity(vx, vy);
|
||||
|
||||
SDL_FRect pos = ball->getPosition();
|
||||
@@ -153,22 +158,24 @@ void BoidManager::applySeparation(Ball* boid, float delta_time) {
|
||||
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;
|
||||
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
|
||||
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 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);
|
||||
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)
|
||||
@@ -186,7 +193,8 @@ void BoidManager::applySeparation(Ball* boid, float delta_time) {
|
||||
steer_y /= count;
|
||||
|
||||
// Aplicar fuerza de separación
|
||||
float vx, vy;
|
||||
float vx;
|
||||
float vy;
|
||||
boid->getVelocity(vx, vy);
|
||||
vx += steer_x * separation_weight_ * delta_time;
|
||||
vy += steer_y * separation_weight_ * delta_time;
|
||||
@@ -201,25 +209,28 @@ void BoidManager::applyAlignment(Ball* boid, float delta_time) {
|
||||
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;
|
||||
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;
|
||||
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 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);
|
||||
float distance = std::sqrt((dx * dx) + (dy * dy));
|
||||
|
||||
if (distance < alignment_radius_) {
|
||||
float other_vx, other_vy;
|
||||
float other_vx;
|
||||
float other_vy;
|
||||
other->getVelocity(other_vx, other_vy);
|
||||
avg_vx += other_vx;
|
||||
avg_vy += other_vy;
|
||||
@@ -233,13 +244,14 @@ void BoidManager::applyAlignment(Ball* boid, float delta_time) {
|
||||
avg_vy /= count;
|
||||
|
||||
// Steering hacia la velocidad promedio
|
||||
float vx, vy;
|
||||
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);
|
||||
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_;
|
||||
@@ -258,22 +270,24 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) {
|
||||
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;
|
||||
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;
|
||||
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 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);
|
||||
float distance = std::sqrt((dx * dx) + (dy * dy));
|
||||
|
||||
if (distance < cohesion_radius_) {
|
||||
center_of_mass_x += other_x;
|
||||
@@ -290,7 +304,7 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) {
|
||||
// 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);
|
||||
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) {
|
||||
@@ -299,13 +313,14 @@ void BoidManager::applyCohesion(Ball* boid, float 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);
|
||||
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, vy;
|
||||
float vx;
|
||||
float vy;
|
||||
boid->getVelocity(vx, vy);
|
||||
vx += steer_x;
|
||||
vy += steer_y;
|
||||
@@ -314,12 +329,12 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) {
|
||||
}
|
||||
}
|
||||
|
||||
void BoidManager::applyBoundaries(Ball* boid) {
|
||||
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 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;
|
||||
@@ -363,11 +378,12 @@ void BoidManager::applyBoundaries(Ball* boid) {
|
||||
|
||||
// Aplicar fuerza de repulsión si hay alguna
|
||||
if (steer_x != 0.0f || steer_y != 0.0f) {
|
||||
float vx, vy;
|
||||
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);
|
||||
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;
|
||||
@@ -381,12 +397,13 @@ void BoidManager::applyBoundaries(Ball* boid) {
|
||||
}
|
||||
}
|
||||
|
||||
void BoidManager::limitSpeed(Ball* boid) {
|
||||
void BoidManager::limitSpeed(Ball* boid) const {
|
||||
// Limitar velocidad máxima del boid
|
||||
float vx, vy;
|
||||
float vx;
|
||||
float vy;
|
||||
boid->getVelocity(vx, vy);
|
||||
|
||||
float speed = std::sqrt(vx * vx + vy * vy);
|
||||
float speed = std::sqrt((vx * vx) + (vy * vy));
|
||||
|
||||
// Limitar velocidad máxima
|
||||
if (speed > max_speed_) {
|
||||
|
||||
@@ -46,8 +46,7 @@ class BoidManager {
|
||||
* @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);
|
||||
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)
|
||||
@@ -105,22 +104,22 @@ class BoidManager {
|
||||
|
||||
// === 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²)
|
||||
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); // Repulsión de bordes (ya no wrapping)
|
||||
void limitSpeed(Ball* boid); // Limitar velocidad máxima
|
||||
void applyBoundaries(Ball* boid) const; // Repulsión de bordes (ya no wrapping)
|
||||
void limitSpeed(Ball* boid) const; // Limitar velocidad máxima
|
||||
};
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
#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) {
|
||||
: 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));
|
||||
@@ -21,7 +21,8 @@ void SpatialGrid::clear() {
|
||||
|
||||
void SpatialGrid::insert(Ball* ball, float x, float y) {
|
||||
// Obtener coordenadas de celda
|
||||
int cell_x, cell_y;
|
||||
int cell_x;
|
||||
int cell_y;
|
||||
getCellCoords(x, y, cell_x, cell_y);
|
||||
|
||||
// Generar hash key y añadir a la celda
|
||||
@@ -29,11 +30,14 @@ void SpatialGrid::insert(Ball* ball, float x, float y) {
|
||||
cells_[key].push_back(ball);
|
||||
}
|
||||
|
||||
std::vector<Ball*> SpatialGrid::queryRadius(float x, float y, float radius) {
|
||||
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, min_cell_y, max_cell_x, max_cell_y;
|
||||
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);
|
||||
|
||||
@@ -82,8 +86,8 @@ void SpatialGrid::getCellCoords(float x, float y, int& cell_x, int& cell_y) cons
|
||||
cell_y = static_cast<int>(std::floor(y / cell_size_));
|
||||
}
|
||||
|
||||
int SpatialGrid::getCellKey(int cell_x, int cell_y) const {
|
||||
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;
|
||||
return (cell_y * grid_cols_) + cell_x;
|
||||
}
|
||||
|
||||
@@ -30,42 +30,42 @@ class Ball; // Forward declaration
|
||||
// ============================================================================
|
||||
|
||||
class SpatialGrid {
|
||||
public:
|
||||
// Constructor: especificar dimensiones del mundo y tamaño de celda
|
||||
SpatialGrid(int world_width, int world_height, float cell_size);
|
||||
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();
|
||||
// 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);
|
||||
// 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);
|
||||
// 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);
|
||||
// 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;
|
||||
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;
|
||||
// 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_;
|
||||
// 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_;
|
||||
// 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_;
|
||||
// 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_;
|
||||
// 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_;
|
||||
};
|
||||
|
||||
@@ -5,22 +5,26 @@
|
||||
#include <vector> // for std::vector in DynamicThemeKeyframe/DynamicTheme
|
||||
|
||||
// Configuración de ventana y pantalla
|
||||
constexpr char WINDOW_CAPTION[] = "ViBe3 Physics (JailDesigner 2025)";
|
||||
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)
|
||||
|
||||
// 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 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)
|
||||
|
||||
@@ -32,7 +36,7 @@ constexpr Uint64 NOTIFICATION_FADE_TIME = 200; // Duración animación salida
|
||||
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";
|
||||
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
|
||||
@@ -51,16 +55,31 @@ constexpr float GRAVITY_CHANGE_LATERAL_MAX = 0.08f; // Velocidad lateral máxim
|
||||
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)
|
||||
// Fase 1 (instanced rendering): límit pràctic ~100K a 60fps (physics bound)
|
||||
constexpr int BALL_COUNT_SCENARIOS[8] = {10, 50, 100, 500, 1000, 5000, 10000, 100000};
|
||||
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)
|
||||
|
||||
// Límites de escenario para modos automáticos (índices en BALL_COUNT_SCENARIOS)
|
||||
// BALL_COUNT_SCENARIOS = {10, 50, 100, 500, 1000, 5000, 10000, 50000}
|
||||
// 0 1 2 3 4 5 6 7
|
||||
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)
|
||||
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 {
|
||||
@@ -165,7 +184,6 @@ enum class ScalingMode {
|
||||
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 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)
|
||||
|
||||
@@ -274,7 +292,6 @@ constexpr int DEMO_LITE_WEIGHT_IMPULSE = 10; // Aplicar impulso (10%)
|
||||
// TOTAL: 100
|
||||
|
||||
// Configuración de Modo LOGO (easter egg - "marca de agua")
|
||||
constexpr int LOGO_MODE_MIN_BALLS = 500; // Mínimo de pelotas para activar modo logo
|
||||
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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,34 +1,32 @@
|
||||
#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 <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 "ui/app_logo.hpp" // for AppLogo
|
||||
#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_shader_preset.hpp" // for NTSCParams, GpuShaderPreset
|
||||
#include "gpu/gpu_sprite_batch.hpp" // for GpuSpriteBatch
|
||||
#include "gpu/gpu_texture.hpp" // for GpuTexture
|
||||
#include "gpu/shader_manager.hpp" // for ShaderManager
|
||||
#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/ui_manager.hpp" // for UIManager
|
||||
#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:
|
||||
@@ -78,16 +76,19 @@ class Engine {
|
||||
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);
|
||||
|
||||
// External shader presets (loaded from data/shaders/)
|
||||
// Cicle PostFX nadiu (OFF → Vinyeta → Scanlines → Cromàtica → Complet)
|
||||
void cycleShader();
|
||||
void setInitialShader(const std::string& name);
|
||||
std::string getActiveShaderName() const;
|
||||
|
||||
// Modo kiosko
|
||||
void setKioskMode(bool enabled) { kiosk_mode_ = enabled; }
|
||||
@@ -96,8 +97,8 @@ class Engine {
|
||||
// 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_; }
|
||||
bool isCustomAutoAvailable() const { return custom_auto_available_; }
|
||||
int getCustomScenarioBalls() const { return custom_scenario_balls_; }
|
||||
|
||||
// Control manual del benchmark (--skip-benchmark, --max-balls)
|
||||
void setSkipBenchmark();
|
||||
@@ -112,10 +113,10 @@ class Engine {
|
||||
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
|
||||
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_; }
|
||||
@@ -132,12 +133,11 @@ class Engine {
|
||||
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; }
|
||||
bool isExternalShaderActive() const { return active_shader_ != nullptr; }
|
||||
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) ===
|
||||
@@ -153,23 +153,23 @@ class Engine {
|
||||
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
|
||||
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::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;
|
||||
SDL_Surface* ui_surface_ = nullptr;
|
||||
|
||||
// Legacy Texture objects — kept for ball physics sizing and AppLogo
|
||||
std::shared_ptr<Texture> texture_ = nullptr; // Textura activa actual
|
||||
@@ -187,19 +187,16 @@ class Engine {
|
||||
|
||||
// PostFX uniforms (passed to GPU each frame)
|
||||
PostFXUniforms postfx_uniforms_ = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
int postfx_effect_mode_ = 0;
|
||||
int postfx_effect_mode_ = 3;
|
||||
bool postfx_enabled_ = false;
|
||||
float postfx_override_vignette_ = -1.f; // -1 = sin override
|
||||
float postfx_override_chroma_ = -1.f;
|
||||
float postfx_override_chroma_ = -1.f;
|
||||
|
||||
// External shader system
|
||||
std::unique_ptr<ShaderManager> shader_manager_;
|
||||
GpuShaderPreset* active_shader_ = nullptr; // null = native PostFX
|
||||
int active_shader_idx_ = -1; // index into shader_manager_->names()
|
||||
std::string initial_shader_name_; // set before initialize()
|
||||
// Sistema de escala de ventana
|
||||
float current_window_scale_ = 1.0f;
|
||||
|
||||
// Sistema de zoom dinámico
|
||||
int current_window_zoom_ = DEFAULT_WINDOW_ZOOM;
|
||||
// Escala del campo de juego lógico (F7/F8)
|
||||
float current_field_scale_ = 1.0f;
|
||||
|
||||
// V-Sync y fullscreen
|
||||
bool vsync_enabled_ = true;
|
||||
@@ -231,10 +228,10 @@ class Engine {
|
||||
int max_auto_scenario_ = 5;
|
||||
|
||||
// Escenario custom (--custom-balls)
|
||||
int custom_scenario_balls_ = 0;
|
||||
int custom_scenario_balls_ = 0;
|
||||
bool custom_scenario_enabled_ = false;
|
||||
bool custom_auto_available_ = false;
|
||||
bool skip_benchmark_ = false;
|
||||
bool custom_auto_available_ = false;
|
||||
bool skip_benchmark_ = false;
|
||||
|
||||
// Bucket sort per z-ordering (SHAPE mode)
|
||||
static constexpr int DEPTH_SORT_BUCKETS = 256;
|
||||
@@ -253,9 +250,9 @@ class Engine {
|
||||
// Sistema de cambio de sprites dinámico
|
||||
void switchTextureInternal(bool show_notification);
|
||||
|
||||
// Sistema de zoom dinámico
|
||||
int calculateMaxWindowZoom() const;
|
||||
void setWindowZoom(int new_zoom);
|
||||
// Sistema de escala de ventana
|
||||
float calculateMaxWindowScale() const;
|
||||
void setWindowScale(float new_scale);
|
||||
void zoomIn();
|
||||
void zoomOut();
|
||||
void updatePhysicalWindowSize();
|
||||
@@ -272,10 +269,12 @@ class Engine {
|
||||
// PostFX helper
|
||||
void applyPostFXPreset(int mode);
|
||||
|
||||
// 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_
|
||||
// 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_
|
||||
};
|
||||
|
||||
6
source/external/texture.cpp
vendored
6
source/external/texture.cpp
vendored
@@ -50,11 +50,7 @@ bool Texture::loadFromFile(const std::string &file_path) {
|
||||
delete[] resourceData; // Liberar buffer temporal
|
||||
|
||||
if (data != nullptr) {
|
||||
if (ResourceManager::isPackLoaded()) {
|
||||
std::cout << "Imagen cargada desde pack: " << filename.c_str() << std::endl;
|
||||
} else {
|
||||
std::cout << "Imagen cargada desde disco: " << filename.c_str() << std::endl;
|
||||
}
|
||||
std::cout << "[Textura] " << filename << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
#include "gpu_ball_buffer.hpp"
|
||||
|
||||
#include <SDL3/SDL_log.h>
|
||||
|
||||
#include <algorithm> // std::min
|
||||
#include <cstring> // memcpy
|
||||
|
||||
bool GpuBallBuffer::init(SDL_GPUDevice* device) {
|
||||
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;
|
||||
buf_info.size = buf_size;
|
||||
gpu_buf_ = SDL_CreateGPUBuffer(device, &buf_info);
|
||||
if (!gpu_buf_) {
|
||||
if (gpu_buf_ == nullptr) {
|
||||
SDL_Log("GpuBallBuffer: GPU buffer creation failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
@@ -20,34 +21,45 @@ bool GpuBallBuffer::init(SDL_GPUDevice* device) {
|
||||
// Transfer buffer (upload staging, cycled every frame)
|
||||
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||
tb_info.size = buf_size;
|
||||
tb_info.size = buf_size;
|
||||
transfer_buf_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
|
||||
if (!transfer_buf_) {
|
||||
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));
|
||||
MAX_BALLS,
|
||||
buf_size / (1024.0f * 1024.0f));
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuBallBuffer::destroy(SDL_GPUDevice* device) {
|
||||
if (!device) return;
|
||||
if (transfer_buf_) { SDL_ReleaseGPUTransferBuffer(device, transfer_buf_); transfer_buf_ = nullptr; }
|
||||
if (gpu_buf_) { SDL_ReleaseGPUBuffer(device, gpu_buf_); gpu_buf_ = nullptr; }
|
||||
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;
|
||||
}
|
||||
|
||||
bool GpuBallBuffer::upload(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd,
|
||||
const BallGPUData* data, int count) {
|
||||
if (!data || count <= 0) { count_ = 0; return false; }
|
||||
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) {
|
||||
if (ptr == nullptr) {
|
||||
SDL_Log("GpuBallBuffer: transfer buffer map failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
@@ -55,8 +67,8 @@ bool GpuBallBuffer::upload(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd,
|
||||
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_GPUTransferBufferLocation src = {transfer_buf_, 0};
|
||||
SDL_GPUBufferRegion dst = {gpu_buf_, 0, upload_size};
|
||||
SDL_UploadToGPUBuffer(copy, &src, &dst, true /* cycle */);
|
||||
SDL_EndGPUCopyPass(copy);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -12,9 +13,9 @@
|
||||
// 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]
|
||||
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");
|
||||
|
||||
@@ -26,22 +27,21 @@ static_assert(sizeof(BallGPUData) == 32, "BallGPUData must be 32 bytes");
|
||||
// // Then in render pass: bind buffer, SDL_DrawGPUPrimitives(pass, 6, count, 0, 0)
|
||||
// ============================================================================
|
||||
class GpuBallBuffer {
|
||||
public:
|
||||
static constexpr int MAX_BALLS = 500000;
|
||||
public:
|
||||
static constexpr int MAX_BALLS = 500000;
|
||||
|
||||
bool init(SDL_GPUDevice* device);
|
||||
void destroy(SDL_GPUDevice* device);
|
||||
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);
|
||||
// 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_; }
|
||||
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;
|
||||
private:
|
||||
SDL_GPUBuffer* gpu_buf_ = nullptr;
|
||||
SDL_GPUTransferBuffer* transfer_buf_ = nullptr;
|
||||
int count_ = 0;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#include "gpu_context.hpp"
|
||||
|
||||
#include <SDL3/SDL_log.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
bool GpuContext::init(SDL_Window* window) {
|
||||
auto GpuContext::init(SDL_Window* window) -> bool {
|
||||
window_ = window;
|
||||
|
||||
// Create GPU device: Metal on Apple, Vulkan elsewhere
|
||||
@@ -13,15 +14,15 @@ bool GpuContext::init(SDL_Window* window) {
|
||||
SDL_GPUShaderFormat preferred = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||
#endif
|
||||
device_ = SDL_CreateGPUDevice(preferred, false, nullptr);
|
||||
if (!device_) {
|
||||
std::cerr << "GpuContext: SDL_CreateGPUDevice failed: " << SDL_GetError() << std::endl;
|
||||
if (device_ == nullptr) {
|
||||
std::cerr << "GpuContext: SDL_CreateGPUDevice failed: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
std::cout << "GpuContext: driver = " << SDL_GetGPUDeviceDriver(device_) << std::endl;
|
||||
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() << std::endl;
|
||||
std::cerr << "GpuContext: SDL_ClaimWindowForGPUDevice failed: " << SDL_GetError() << '\n';
|
||||
SDL_DestroyGPUDevice(device_);
|
||||
device_ = nullptr;
|
||||
return false;
|
||||
@@ -29,17 +30,15 @@ bool GpuContext::init(SDL_Window* window) {
|
||||
|
||||
// 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_) << std::endl;
|
||||
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);
|
||||
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, SDL_GPU_PRESENTMODE_VSYNC);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuContext::destroy() {
|
||||
if (device_) {
|
||||
if (device_ != nullptr) {
|
||||
SDL_WaitForGPUIdle(device_);
|
||||
SDL_ReleaseWindowFromGPUDevice(device_, window_);
|
||||
SDL_DestroyGPUDevice(device_);
|
||||
@@ -48,16 +47,17 @@ void GpuContext::destroy() {
|
||||
window_ = nullptr;
|
||||
}
|
||||
|
||||
SDL_GPUCommandBuffer* GpuContext::acquireCommandBuffer() {
|
||||
auto GpuContext::acquireCommandBuffer() -> SDL_GPUCommandBuffer* {
|
||||
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
|
||||
if (!cmd) {
|
||||
if (cmd == nullptr) {
|
||||
SDL_Log("GpuContext: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError());
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
SDL_GPUTexture* GpuContext::acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf,
|
||||
Uint32* out_w, Uint32* out_h) {
|
||||
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());
|
||||
@@ -71,10 +71,8 @@ void GpuContext::submit(SDL_GPUCommandBuffer* cmd_buf) {
|
||||
SDL_SubmitGPUCommandBuffer(cmd_buf);
|
||||
}
|
||||
|
||||
bool GpuContext::setVSync(bool enabled) {
|
||||
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);
|
||||
return SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, mode);
|
||||
}
|
||||
|
||||
@@ -8,26 +8,27 @@
|
||||
// Replaces SDL_Renderer as the main rendering backend.
|
||||
// ============================================================================
|
||||
class GpuContext {
|
||||
public:
|
||||
bool init(SDL_Window* window);
|
||||
void destroy();
|
||||
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_; }
|
||||
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);
|
||||
void submit(SDL_GPUCommandBuffer* cmd_buf);
|
||||
// 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);
|
||||
// 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;
|
||||
private:
|
||||
SDL_GPUDevice* device_ = nullptr;
|
||||
SDL_Window* window_ = nullptr;
|
||||
SDL_GPUTextureFormat swapchain_format_ = SDL_GPU_TEXTUREFORMAT_INVALID;
|
||||
};
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
#include "gpu_pipeline.hpp"
|
||||
#include "gpu_sprite_batch.hpp" // for GpuVertex layout
|
||||
#include "gpu_ball_buffer.hpp" // for BallGPUData layout
|
||||
|
||||
#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 "sprite_vert_spv.h"
|
||||
#include "sprite_frag_spv.h"
|
||||
#include "postfx_vert_spv.h"
|
||||
#include "postfx_frag_spv.h"
|
||||
#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)
|
||||
// ============================================================================
|
||||
@@ -116,7 +120,7 @@ struct PostFXUniforms {
|
||||
float vignette_strength;
|
||||
float chroma_strength;
|
||||
float scanline_strength;
|
||||
float time;
|
||||
float screen_height;
|
||||
};
|
||||
|
||||
fragment float4 postfx_fs(PostVOut in [[stage_in]],
|
||||
@@ -132,7 +136,7 @@ fragment float4 postfx_fs(PostVOut in [[stage_in]],
|
||||
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 * 720.0);
|
||||
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
|
||||
@@ -197,14 +201,15 @@ vertex BallVOut ball_instanced_vs(BallInstance inst [[stage_in]],
|
||||
return out;
|
||||
}
|
||||
)";
|
||||
#endif // __APPLE__
|
||||
|
||||
// ============================================================================
|
||||
// GpuPipeline implementation
|
||||
// ============================================================================
|
||||
|
||||
bool GpuPipeline::init(SDL_GPUDevice* device,
|
||||
SDL_GPUTextureFormat target_format,
|
||||
SDL_GPUTextureFormat offscreen_format) {
|
||||
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)) {
|
||||
@@ -212,7 +217,7 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (!(supported & SDL_GPU_SHADERFORMAT_SPIRV)) {
|
||||
if ((supported & SDL_GPU_SHADERFORMAT_SPIRV) == 0u) {
|
||||
SDL_Log("GpuPipeline: SPIRV not supported (format mask=%u)", supported);
|
||||
return false;
|
||||
}
|
||||
@@ -222,81 +227,81 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
|
||||
// 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);
|
||||
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);
|
||||
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 || !sprite_frag) {
|
||||
if ((sprite_vert == nullptr) || (sprite_frag == nullptr)) {
|
||||
SDL_Log("GpuPipeline: failed to create sprite shaders");
|
||||
if (sprite_vert) SDL_ReleaseGPUShader(device, sprite_vert);
|
||||
if (sprite_frag) SDL_ReleaseGPUShader(device, sprite_frag);
|
||||
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.slot = 0;
|
||||
vb_desc.pitch = sizeof(GpuVertex);
|
||||
vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;
|
||||
vb_desc.instance_step_rate = 0;
|
||||
|
||||
SDL_GPUVertexAttribute attrs[3] = {};
|
||||
attrs[0].location = 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[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
||||
attrs[0].offset = static_cast<Uint32>(offsetof(GpuVertex, x));
|
||||
|
||||
attrs[1].location = 1;
|
||||
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[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
||||
attrs[1].offset = static_cast<Uint32>(offsetof(GpuVertex, u));
|
||||
|
||||
attrs[2].location = 2;
|
||||
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));
|
||||
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;
|
||||
vertex_input.num_vertex_attributes = 3;
|
||||
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
|
||||
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.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_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_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_) {
|
||||
if (sprite_pipeline_ == nullptr) {
|
||||
SDL_Log("GpuPipeline: sprite pipeline creation failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
@@ -308,59 +313,59 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
|
||||
// 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);
|
||||
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);
|
||||
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 || !ball_frag) {
|
||||
if ((ball_vert == nullptr) || (ball_frag == nullptr)) {
|
||||
SDL_Log("GpuPipeline: failed to create ball instanced shaders");
|
||||
if (ball_vert) SDL_ReleaseGPUShader(device, ball_vert);
|
||||
if (ball_frag) SDL_ReleaseGPUShader(device, ball_frag);
|
||||
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.slot = 0;
|
||||
ball_vb_desc.pitch = sizeof(BallGPUData);
|
||||
ball_vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_INSTANCE;
|
||||
ball_vb_desc.instance_step_rate = 1;
|
||||
|
||||
SDL_GPUVertexAttribute ball_attrs[3] = {};
|
||||
std::array<SDL_GPUVertexAttribute, 3> ball_attrs = {};
|
||||
// attr 0: center (float2) at offset 0
|
||||
ball_attrs[0].location = 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));
|
||||
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].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));
|
||||
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].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));
|
||||
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;
|
||||
ball_vertex_input.num_vertex_attributes = 3;
|
||||
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_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.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);
|
||||
@@ -368,7 +373,7 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
|
||||
SDL_ReleaseGPUShader(device, ball_vert);
|
||||
SDL_ReleaseGPUShader(device, ball_frag);
|
||||
|
||||
if (!ball_pipeline_) {
|
||||
if (ball_pipeline_ == nullptr) {
|
||||
SDL_Log("GpuPipeline: ball instanced pipeline creation failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
@@ -387,20 +392,20 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
|
||||
// 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);
|
||||
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);
|
||||
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 || !postfx_frag) {
|
||||
if ((postfx_vert == nullptr) || (postfx_frag == nullptr)) {
|
||||
SDL_Log("GpuPipeline: failed to create postfx shaders");
|
||||
if (postfx_vert) SDL_ReleaseGPUShader(device, postfx_vert);
|
||||
if (postfx_frag) SDL_ReleaseGPUShader(device, postfx_frag);
|
||||
if (postfx_vert != nullptr) {
|
||||
SDL_ReleaseGPUShader(device, postfx_vert);
|
||||
}
|
||||
if (postfx_frag != nullptr) {
|
||||
SDL_ReleaseGPUShader(device, postfx_frag);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -410,17 +415,17 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
|
||||
no_blend.enable_color_write_mask = false;
|
||||
|
||||
SDL_GPUColorTargetDescription postfx_target_desc = {};
|
||||
postfx_target_desc.format = target_format;
|
||||
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_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.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);
|
||||
@@ -428,7 +433,7 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
|
||||
SDL_ReleaseGPUShader(device, postfx_vert);
|
||||
SDL_ReleaseGPUShader(device, postfx_frag);
|
||||
|
||||
if (!postfx_pipeline_) {
|
||||
if (postfx_pipeline_ == nullptr) {
|
||||
SDL_Log("GpuPipeline: postfx pipeline creation failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
@@ -438,55 +443,65 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
|
||||
}
|
||||
|
||||
void GpuPipeline::destroy(SDL_GPUDevice* device) {
|
||||
if (sprite_pipeline_) { SDL_ReleaseGPUGraphicsPipeline(device, sprite_pipeline_); sprite_pipeline_ = nullptr; }
|
||||
if (ball_pipeline_) { SDL_ReleaseGPUGraphicsPipeline(device, ball_pipeline_); ball_pipeline_ = nullptr; }
|
||||
if (postfx_pipeline_) { SDL_ReleaseGPUGraphicsPipeline(device, postfx_pipeline_); postfx_pipeline_ = nullptr; }
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_GPUShader* 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) {
|
||||
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.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;
|
||||
info.num_storage_buffers = num_storage_buffers;
|
||||
info.num_uniform_buffers = num_uniform_buffers;
|
||||
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
|
||||
if (!shader)
|
||||
if (shader == nullptr) {
|
||||
SDL_Log("GpuPipeline: SPIRV shader '%s' failed: %s", entrypoint, SDL_GetError());
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
SDL_GPUShader* 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) {
|
||||
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.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;
|
||||
info.num_storage_buffers = num_storage_buffers;
|
||||
info.num_uniform_buffers = num_uniform_buffers;
|
||||
|
||||
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
|
||||
if (!shader) {
|
||||
if (shader == nullptr) {
|
||||
SDL_Log("GpuPipeline: shader '%s' failed: %s", entrypoint, SDL_GetError());
|
||||
}
|
||||
return shader;
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
// MSL binding: constant PostFXUniforms& u [[buffer(0)]]
|
||||
// ============================================================================
|
||||
struct PostFXUniforms {
|
||||
float vignette_strength; // 0 = none, 1.5 = default subtle
|
||||
float chroma_strength; // 0 = off, 1 = full chromatic aberration
|
||||
float scanline_strength; // 0 = off, 1 = full scanlines
|
||||
float time; // accumulated seconds (for future animations)
|
||||
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
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -27,37 +27,37 @@ struct PostFXUniforms {
|
||||
// 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);
|
||||
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_; }
|
||||
SDL_GPUGraphicsPipeline* spritePipeline() const { return sprite_pipeline_; }
|
||||
SDL_GPUGraphicsPipeline* ballPipeline() const { return ball_pipeline_; }
|
||||
SDL_GPUGraphicsPipeline* postfxPipeline() const { return postfx_pipeline_; }
|
||||
|
||||
private:
|
||||
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);
|
||||
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);
|
||||
|
||||
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);
|
||||
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;
|
||||
SDL_GPUGraphicsPipeline* sprite_pipeline_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* ball_pipeline_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* postfx_pipeline_ = nullptr;
|
||||
};
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
#include "gpu_shader_preset.hpp"
|
||||
#include "gpu_texture.hpp"
|
||||
|
||||
#include <SDL3/SDL_log.h>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
static std::vector<uint8_t> readFile(const std::string& path) {
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
if (!f) return {};
|
||||
std::streamsize sz = f.tellg();
|
||||
f.seekg(0, std::ios::beg);
|
||||
std::vector<uint8_t> buf(static_cast<size_t>(sz));
|
||||
if (!f.read(reinterpret_cast<char*>(buf.data()), sz)) return {};
|
||||
return buf;
|
||||
}
|
||||
|
||||
static std::string trim(const std::string& s) {
|
||||
size_t a = s.find_first_not_of(" \t\r\n");
|
||||
if (a == std::string::npos) return {};
|
||||
size_t b = s.find_last_not_of(" \t\r\n");
|
||||
return s.substr(a, b - a + 1);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GpuShaderPreset
|
||||
// ============================================================================
|
||||
|
||||
bool GpuShaderPreset::parseIni(const std::string& ini_path) {
|
||||
std::ifstream f(ini_path);
|
||||
if (!f) {
|
||||
SDL_Log("GpuShaderPreset: cannot open %s", ini_path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
int num_passes = 0;
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
// Strip comments
|
||||
auto comment = line.find(';');
|
||||
if (comment != std::string::npos) line = line.substr(0, comment);
|
||||
line = trim(line);
|
||||
if (line.empty()) continue;
|
||||
|
||||
auto eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
|
||||
std::string key = trim(line.substr(0, eq));
|
||||
std::string value = trim(line.substr(eq + 1));
|
||||
if (key.empty() || value.empty()) continue;
|
||||
|
||||
if (key == "name") {
|
||||
name_ = value;
|
||||
} else if (key == "passes") {
|
||||
num_passes = std::stoi(value);
|
||||
} else {
|
||||
// Try to parse as float parameter
|
||||
try {
|
||||
params_[key] = std::stof(value);
|
||||
} catch (...) {
|
||||
// Non-float values stored separately (pass0_vert etc.)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (num_passes <= 0) {
|
||||
SDL_Log("GpuShaderPreset: no passes defined in %s", ini_path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Second pass: read per-pass file names
|
||||
f.clear();
|
||||
f.seekg(0, std::ios::beg);
|
||||
descs_.resize(num_passes);
|
||||
while (std::getline(f, line)) {
|
||||
auto comment = line.find(';');
|
||||
if (comment != std::string::npos) line = line.substr(0, comment);
|
||||
line = trim(line);
|
||||
if (line.empty()) continue;
|
||||
auto eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
std::string key = trim(line.substr(0, eq));
|
||||
std::string value = trim(line.substr(eq + 1));
|
||||
|
||||
for (int i = 0; i < num_passes; ++i) {
|
||||
std::string vi = "pass" + std::to_string(i) + "_vert";
|
||||
std::string fi = "pass" + std::to_string(i) + "_frag";
|
||||
if (key == vi) descs_[i].vert_name = value;
|
||||
if (key == fi) descs_[i].frag_name = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate
|
||||
for (int i = 0; i < num_passes; ++i) {
|
||||
if (descs_[i].vert_name.empty() || descs_[i].frag_name.empty()) {
|
||||
SDL_Log("GpuShaderPreset: pass %d missing vert or frag in %s", i, ini_path.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
SDL_GPUGraphicsPipeline* GpuShaderPreset::buildPassPipeline(SDL_GPUDevice* device,
|
||||
const std::string& vert_spv_path,
|
||||
const std::string& frag_spv_path,
|
||||
SDL_GPUTextureFormat target_fmt) {
|
||||
auto vert_spv = readFile(vert_spv_path);
|
||||
auto frag_spv = readFile(frag_spv_path);
|
||||
if (vert_spv.empty()) {
|
||||
SDL_Log("GpuShaderPreset: cannot read %s", vert_spv_path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
if (frag_spv.empty()) {
|
||||
SDL_Log("GpuShaderPreset: cannot read %s", frag_spv_path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SDL_GPUShaderCreateInfo vert_info = {};
|
||||
vert_info.code = vert_spv.data();
|
||||
vert_info.code_size = vert_spv.size();
|
||||
vert_info.entrypoint = "main";
|
||||
vert_info.format = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||
vert_info.stage = SDL_GPU_SHADERSTAGE_VERTEX;
|
||||
vert_info.num_samplers = 0;
|
||||
vert_info.num_uniform_buffers = 0;
|
||||
|
||||
SDL_GPUShaderCreateInfo frag_info = {};
|
||||
frag_info.code = frag_spv.data();
|
||||
frag_info.code_size = frag_spv.size();
|
||||
frag_info.entrypoint = "main";
|
||||
frag_info.format = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||
frag_info.stage = SDL_GPU_SHADERSTAGE_FRAGMENT;
|
||||
frag_info.num_samplers = 1;
|
||||
frag_info.num_uniform_buffers = 1;
|
||||
|
||||
SDL_GPUShader* vert_shader = SDL_CreateGPUShader(device, &vert_info);
|
||||
SDL_GPUShader* frag_shader = SDL_CreateGPUShader(device, &frag_info);
|
||||
|
||||
if (!vert_shader || !frag_shader) {
|
||||
SDL_Log("GpuShaderPreset: shader creation failed for %s / %s: %s",
|
||||
vert_spv_path.c_str(), frag_spv_path.c_str(), SDL_GetError());
|
||||
if (vert_shader) SDL_ReleaseGPUShader(device, vert_shader);
|
||||
if (frag_shader) SDL_ReleaseGPUShader(device, frag_shader);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Full-screen triangle: no vertex input, no blend
|
||||
SDL_GPUColorTargetBlendState no_blend = {};
|
||||
no_blend.enable_blend = false;
|
||||
no_blend.enable_color_write_mask = false;
|
||||
|
||||
SDL_GPUColorTargetDescription ct_desc = {};
|
||||
ct_desc.format = target_fmt;
|
||||
ct_desc.blend_state = no_blend;
|
||||
|
||||
SDL_GPUVertexInputState no_input = {};
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo pipe_info = {};
|
||||
pipe_info.vertex_shader = vert_shader;
|
||||
pipe_info.fragment_shader = frag_shader;
|
||||
pipe_info.vertex_input_state = no_input;
|
||||
pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
pipe_info.target_info.num_color_targets = 1;
|
||||
pipe_info.target_info.color_target_descriptions = &ct_desc;
|
||||
|
||||
SDL_GPUGraphicsPipeline* pipeline = SDL_CreateGPUGraphicsPipeline(device, &pipe_info);
|
||||
|
||||
SDL_ReleaseGPUShader(device, vert_shader);
|
||||
SDL_ReleaseGPUShader(device, frag_shader);
|
||||
|
||||
if (!pipeline)
|
||||
SDL_Log("GpuShaderPreset: pipeline creation failed: %s", SDL_GetError());
|
||||
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
bool GpuShaderPreset::load(SDL_GPUDevice* device,
|
||||
const std::string& dir,
|
||||
SDL_GPUTextureFormat swapchain_fmt,
|
||||
int w, int h) {
|
||||
dir_ = dir;
|
||||
swapchain_fmt_ = swapchain_fmt;
|
||||
|
||||
// Parse ini
|
||||
if (!parseIni(dir + "/preset.ini"))
|
||||
return false;
|
||||
|
||||
int n = static_cast<int>(descs_.size());
|
||||
passes_.resize(n);
|
||||
|
||||
// Intermediate render target format (signed float to handle NTSC signal range)
|
||||
SDL_GPUTextureFormat inter_fmt = SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT;
|
||||
|
||||
for (int i = 0; i < n; ++i) {
|
||||
bool is_last = (i == n - 1);
|
||||
SDL_GPUTextureFormat target_fmt = is_last ? swapchain_fmt : inter_fmt;
|
||||
|
||||
std::string vert_spv = dir + "/" + descs_[i].vert_name + ".spv";
|
||||
std::string frag_spv = dir + "/" + descs_[i].frag_name + ".spv";
|
||||
|
||||
passes_[i].pipeline = buildPassPipeline(device, vert_spv, frag_spv, target_fmt);
|
||||
if (!passes_[i].pipeline) {
|
||||
SDL_Log("GpuShaderPreset: failed to build pipeline for pass %d", i);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_last) {
|
||||
// Create intermediate render target
|
||||
auto tex = std::make_unique<GpuTexture>();
|
||||
if (!tex->createRenderTarget(device, w, h, inter_fmt)) {
|
||||
SDL_Log("GpuShaderPreset: failed to create intermediate target for pass %d", i);
|
||||
return false;
|
||||
}
|
||||
passes_[i].target = tex.get();
|
||||
targets_.push_back(std::move(tex));
|
||||
}
|
||||
// Last pass: target = null (caller binds swapchain)
|
||||
}
|
||||
|
||||
SDL_Log("GpuShaderPreset: loaded '%s' (%d passes)", name_.c_str(), n);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuShaderPreset::recreateTargets(SDL_GPUDevice* device, int w, int h) {
|
||||
SDL_GPUTextureFormat inter_fmt = SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT;
|
||||
for (auto& tex : targets_) {
|
||||
tex->destroy(device);
|
||||
tex->createRenderTarget(device, w, h, inter_fmt);
|
||||
}
|
||||
}
|
||||
|
||||
void GpuShaderPreset::destroy(SDL_GPUDevice* device) {
|
||||
for (auto& pass : passes_) {
|
||||
if (pass.pipeline) {
|
||||
SDL_ReleaseGPUGraphicsPipeline(device, pass.pipeline);
|
||||
pass.pipeline = nullptr;
|
||||
}
|
||||
}
|
||||
for (auto& tex : targets_) {
|
||||
if (tex) tex->destroy(device);
|
||||
}
|
||||
targets_.clear();
|
||||
passes_.clear();
|
||||
descs_.clear();
|
||||
params_.clear();
|
||||
}
|
||||
|
||||
float GpuShaderPreset::param(const std::string& key, float default_val) const {
|
||||
auto it = params_.find(key);
|
||||
return (it != params_.end()) ? it->second : default_val;
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "gpu_texture.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// NTSCParams — uniform buffer for NTSC shader passes (set=3, binding=0)
|
||||
// Matches the layout in pass0_encode.frag and pass1_decode.frag.
|
||||
// Pushed via SDL_PushGPUFragmentUniformData(cmd, 0, &ntsc, sizeof(NTSCParams)).
|
||||
// ============================================================================
|
||||
struct NTSCParams {
|
||||
float source_width;
|
||||
float source_height;
|
||||
float a_value;
|
||||
float b_value;
|
||||
float cc_value;
|
||||
float scan_time;
|
||||
float notch_width;
|
||||
float y_freq;
|
||||
float i_freq;
|
||||
float q_freq;
|
||||
float _pad[2];
|
||||
};
|
||||
static_assert(sizeof(NTSCParams) == 48, "NTSCParams must be 48 bytes");
|
||||
|
||||
// ============================================================================
|
||||
// ShaderPass — one render pass in a multi-pass shader preset
|
||||
// ============================================================================
|
||||
struct ShaderPass {
|
||||
SDL_GPUGraphicsPipeline* pipeline = nullptr;
|
||||
GpuTexture* target = nullptr; // null = swapchain (last pass)
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// GpuShaderPreset — loads and owns a multi-pass shader preset from disk.
|
||||
//
|
||||
// Directory layout:
|
||||
// <dir>/preset.ini — descriptor
|
||||
// <dir>/pass0_xxx.vert — GLSL 4.50 vertex shader source
|
||||
// <dir>/pass0_xxx.frag — GLSL 4.50 fragment shader source
|
||||
// <dir>/pass0_xxx.vert.spv — compiled SPIRV (by CMake/glslc at build time)
|
||||
// <dir>/pass0_xxx.frag.spv — compiled SPIRV
|
||||
// ...
|
||||
// ============================================================================
|
||||
class GpuShaderPreset {
|
||||
public:
|
||||
// Load preset from directory. swapchain_fmt is the target format for the
|
||||
// last pass; intermediate passes use R16G16B16A16_FLOAT.
|
||||
bool load(SDL_GPUDevice* device,
|
||||
const std::string& dir,
|
||||
SDL_GPUTextureFormat swapchain_fmt,
|
||||
int w, int h);
|
||||
|
||||
void destroy(SDL_GPUDevice* device);
|
||||
|
||||
// Recreate intermediate render targets on resolution change.
|
||||
void recreateTargets(SDL_GPUDevice* device, int w, int h);
|
||||
|
||||
int passCount() const { return static_cast<int>(passes_.size()); }
|
||||
ShaderPass& pass(int i) { return passes_[i]; }
|
||||
|
||||
const std::string& name() const { return name_; }
|
||||
|
||||
// Read a float parameter parsed from preset.ini (returns default_val if absent).
|
||||
float param(const std::string& key, float default_val) const;
|
||||
|
||||
private:
|
||||
std::vector<ShaderPass> passes_;
|
||||
std::vector<std::unique_ptr<GpuTexture>> targets_; // intermediate render targets
|
||||
std::string name_;
|
||||
std::string dir_;
|
||||
std::unordered_map<std::string, float> params_;
|
||||
SDL_GPUTextureFormat swapchain_fmt_ = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
|
||||
|
||||
// Entries read from preset.ini for each pass
|
||||
struct PassDesc {
|
||||
std::string vert_name; // e.g. "pass0_encode.vert"
|
||||
std::string frag_name; // e.g. "pass0_encode.frag"
|
||||
};
|
||||
std::vector<PassDesc> descs_;
|
||||
|
||||
bool parseIni(const std::string& ini_path);
|
||||
|
||||
// Build a full-screen-triangle pipeline from two on-disk SPV files.
|
||||
SDL_GPUGraphicsPipeline* buildPassPipeline(SDL_GPUDevice* device,
|
||||
const std::string& vert_spv_path,
|
||||
const std::string& frag_spv_path,
|
||||
SDL_GPUTextureFormat target_fmt);
|
||||
};
|
||||
@@ -1,26 +1,29 @@
|
||||
#include "gpu_sprite_batch.hpp"
|
||||
|
||||
#include <SDL3/SDL_log.h>
|
||||
|
||||
#include <cstring> // memcpy
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public interface
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool GpuSpriteBatch::init(SDL_GPUDevice* device) {
|
||||
// Pre-allocate GPU buffers large enough for MAX_SPRITES quads.
|
||||
Uint32 max_verts = static_cast<Uint32>(MAX_SPRITES) * 4;
|
||||
Uint32 max_indices = static_cast<Uint32>(MAX_SPRITES) * 6;
|
||||
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 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;
|
||||
vb_info.size = vb_size;
|
||||
vertex_buf_ = SDL_CreateGPUBuffer(device, &vb_info);
|
||||
if (!vertex_buf_) {
|
||||
if (vertex_buf_ == nullptr) {
|
||||
SDL_Log("GpuSpriteBatch: vertex buffer creation failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
@@ -28,9 +31,9 @@ bool GpuSpriteBatch::init(SDL_GPUDevice* device) {
|
||||
// Index buffer
|
||||
SDL_GPUBufferCreateInfo ib_info = {};
|
||||
ib_info.usage = SDL_GPU_BUFFERUSAGE_INDEX;
|
||||
ib_info.size = ib_size;
|
||||
ib_info.size = ib_size;
|
||||
index_buf_ = SDL_CreateGPUBuffer(device, &ib_info);
|
||||
if (!index_buf_) {
|
||||
if (index_buf_ == nullptr) {
|
||||
SDL_Log("GpuSpriteBatch: index buffer creation failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
@@ -39,87 +42,104 @@ bool GpuSpriteBatch::init(SDL_GPUDevice* device) {
|
||||
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||
|
||||
tb_info.size = vb_size;
|
||||
tb_info.size = vb_size;
|
||||
vertex_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
|
||||
if (!vertex_transfer_) {
|
||||
if (vertex_transfer_ == nullptr) {
|
||||
SDL_Log("GpuSpriteBatch: vertex transfer buffer failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
tb_info.size = ib_size;
|
||||
tb_info.size = ib_size;
|
||||
index_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
|
||||
if (!index_transfer_) {
|
||||
if (index_transfer_ == nullptr) {
|
||||
SDL_Log("GpuSpriteBatch: index transfer buffer failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
vertices_.reserve(MAX_SPRITES * 4);
|
||||
indices_.reserve(MAX_SPRITES * 6);
|
||||
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) return;
|
||||
if (vertex_transfer_) { SDL_ReleaseGPUTransferBuffer(device, vertex_transfer_); vertex_transfer_ = nullptr; }
|
||||
if (index_transfer_) { SDL_ReleaseGPUTransferBuffer(device, index_transfer_); index_transfer_ = nullptr; }
|
||||
if (vertex_buf_) { SDL_ReleaseGPUBuffer(device, vertex_buf_); vertex_buf_ = nullptr; }
|
||||
if (index_buf_) { SDL_ReleaseGPUBuffer(device, index_buf_); index_buf_ = nullptr; }
|
||||
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;
|
||||
bg_index_count_ = 0;
|
||||
sprite_index_offset_ = 0;
|
||||
sprite_index_count_ = 0;
|
||||
overlay_index_offset_ = 0;
|
||||
overlay_index_count_ = 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) {
|
||||
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).
|
||||
uint32_t vi = static_cast<uint32_t>(vertices_.size());
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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);
|
||||
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;
|
||||
bg_index_count_ = 6;
|
||||
sprite_index_offset_ = 6;
|
||||
|
||||
(void)screen_w; (void)screen_h; // unused — bg always covers full NDC
|
||||
(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) {
|
||||
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 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, ndy0, ndx1, ndy1;
|
||||
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);
|
||||
|
||||
@@ -128,37 +148,57 @@ void GpuSpriteBatch::addSprite(float x, float y, float w, float h,
|
||||
}
|
||||
|
||||
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());
|
||||
pushQuad(-1.0f, 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f);
|
||||
overlay_index_count_ = 6;
|
||||
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;
|
||||
}
|
||||
|
||||
bool GpuSpriteBatch::uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf) {
|
||||
if (vertices_.empty()) return false;
|
||||
auto GpuSpriteBatch::uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf) -> bool {
|
||||
if (vertices_.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Uint32 vb_size = static_cast<Uint32>(vertices_.size() * sizeof(GpuVertex));
|
||||
Uint32 ib_size = static_cast<Uint32>(indices_.size() * sizeof(uint32_t));
|
||||
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) { SDL_Log("GpuSpriteBatch: vertex map failed"); return false; }
|
||||
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) { SDL_Log("GpuSpriteBatch: index map failed"); return false; }
|
||||
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_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_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);
|
||||
@@ -169,25 +209,28 @@ bool GpuSpriteBatch::uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cm
|
||||
// Private helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void GpuSpriteBatch::toNDC(float px, float py,
|
||||
float screen_w, float screen_h,
|
||||
float& ndx, float& ndy) const {
|
||||
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) {
|
||||
if (vertices_.size() + 4 > static_cast<size_t>(MAX_SPRITES) * 4) return;
|
||||
uint32_t vi = static_cast<uint32_t>(vertices_.size());
|
||||
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 });
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <vector>
|
||||
|
||||
#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)
|
||||
float x, y; // NDC position (−1..1)
|
||||
float u, v; // Texture coords (0..1)
|
||||
float r, g, b, a; // RGBA color (0..1)
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -25,62 +26,56 @@ struct GpuVertex {
|
||||
// // Then in render pass: bind buffers, draw bg with white tex, draw sprites.
|
||||
// ============================================================================
|
||||
class GpuSpriteBatch {
|
||||
public:
|
||||
// Maximum sprites (background + UI overlay each count as one sprite)
|
||||
static constexpr int MAX_SPRITES = 200000;
|
||||
public:
|
||||
// Default maximum sprites (background + UI overlay each count as one sprite)
|
||||
static constexpr int DEFAULT_MAX_SPRITES = 200000;
|
||||
|
||||
bool init(SDL_GPUDevice* device);
|
||||
void destroy(SDL_GPUDevice* device);
|
||||
bool init(SDL_GPUDevice* device, int max_sprites = DEFAULT_MAX_SPRITES);
|
||||
void destroy(SDL_GPUDevice* device);
|
||||
|
||||
void beginFrame();
|
||||
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 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 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();
|
||||
// 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);
|
||||
// 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(); }
|
||||
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:
|
||||
void toNDC(float px, float py, float screen_w, float screen_h,
|
||||
float& ndx, float& ndy) const;
|
||||
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);
|
||||
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_;
|
||||
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;
|
||||
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 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;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <SDL3/SDL_log.h>
|
||||
#include <SDL3/SDL_pixels.h>
|
||||
|
||||
#include <array> // for std::array
|
||||
#include <cstring> // memcpy
|
||||
#include <string>
|
||||
|
||||
@@ -13,7 +15,7 @@
|
||||
// Public interface
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) {
|
||||
auto GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) -> bool {
|
||||
unsigned char* resource_data = nullptr;
|
||||
size_t resource_size = 0;
|
||||
|
||||
@@ -22,15 +24,22 @@ bool GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int w = 0, h = 0, orig = 0;
|
||||
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);
|
||||
resource_data,
|
||||
static_cast<int>(resource_size),
|
||||
&w,
|
||||
&h,
|
||||
&orig,
|
||||
STBI_rgb_alpha);
|
||||
delete[] resource_data;
|
||||
|
||||
if (!pixels) {
|
||||
if (pixels == nullptr) {
|
||||
SDL_Log("GpuTexture: stbi decode failed for '%s': %s",
|
||||
file_path.c_str(), stbi_failure_reason());
|
||||
file_path.c_str(),
|
||||
stbi_failure_reason());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -44,15 +53,17 @@ bool GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool GpuTexture::fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest) {
|
||||
if (!surface) return false;
|
||||
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) {
|
||||
if (rgba == nullptr) {
|
||||
SDL_Log("GpuTexture: SDL_ConvertSurface failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
@@ -60,55 +71,65 @@ bool GpuTexture::fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool n
|
||||
}
|
||||
|
||||
destroy(device);
|
||||
bool ok = uploadPixels(device, rgba->pixels, rgba->w, rgba->h,
|
||||
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
|
||||
if (ok) ok = createSampler(device, nearest);
|
||||
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);
|
||||
if (need_free) {
|
||||
SDL_DestroySurface(rgba);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool GpuTexture::createRenderTarget(SDL_GPUDevice* device, int w, int h,
|
||||
SDL_GPUTextureFormat format) {
|
||||
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.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;
|
||||
info.num_levels = 1;
|
||||
info.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||
|
||||
texture_ = SDL_CreateGPUTexture(device, &info);
|
||||
if (!texture_) {
|
||||
if (texture_ == nullptr) {
|
||||
SDL_Log("GpuTexture: createRenderTarget failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
width_ = w;
|
||||
width_ = w;
|
||||
height_ = h;
|
||||
|
||||
// Render targets are sampled with linear filter (postfx reads them)
|
||||
return createSampler(device, false);
|
||||
}
|
||||
|
||||
bool GpuTexture::createWhite(SDL_GPUDevice* device) {
|
||||
auto GpuTexture::createWhite(SDL_GPUDevice* device) -> bool {
|
||||
destroy(device);
|
||||
// 1×1 white RGBA pixel
|
||||
const Uint8 white[4] = {255, 255, 255, 255};
|
||||
bool ok = uploadPixels(device, white, 1, 1,
|
||||
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
|
||||
if (ok) ok = createSampler(device, true);
|
||||
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) return;
|
||||
if (sampler_) { SDL_ReleaseGPUSampler(device, sampler_); sampler_ = nullptr; }
|
||||
if (texture_) { SDL_ReleaseGPUTexture(device, texture_); texture_ = nullptr; }
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -116,34 +137,33 @@ void GpuTexture::destroy(SDL_GPUDevice* device) {
|
||||
// Private helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels,
|
||||
int w, int h, SDL_GPUTextureFormat format) {
|
||||
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.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;
|
||||
tex_info.num_levels = 1;
|
||||
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||
|
||||
texture_ = SDL_CreateGPUTexture(device, &tex_info);
|
||||
if (!texture_) {
|
||||
if (texture_ == nullptr) {
|
||||
SDL_Log("GpuTexture: SDL_CreateGPUTexture failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create transfer buffer and upload pixels
|
||||
Uint32 data_size = static_cast<Uint32>(w * h * 4); // RGBA = 4 bytes/pixel
|
||||
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;
|
||||
tb_info.size = data_size;
|
||||
|
||||
SDL_GPUTransferBuffer* transfer = SDL_CreateGPUTransferBuffer(device, &tb_info);
|
||||
if (!transfer) {
|
||||
if (transfer == nullptr) {
|
||||
SDL_Log("GpuTexture: transfer buffer creation failed: %s", SDL_GetError());
|
||||
SDL_ReleaseGPUTexture(device, texture_);
|
||||
texture_ = nullptr;
|
||||
@@ -151,7 +171,7 @@ bool GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels,
|
||||
}
|
||||
|
||||
void* mapped = SDL_MapGPUTransferBuffer(device, transfer, false);
|
||||
if (!mapped) {
|
||||
if (mapped == nullptr) {
|
||||
SDL_Log("GpuTexture: map failed: %s", SDL_GetError());
|
||||
SDL_ReleaseGPUTransferBuffer(device, transfer);
|
||||
SDL_ReleaseGPUTexture(device, texture_);
|
||||
@@ -167,14 +187,14 @@ bool GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels,
|
||||
|
||||
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);
|
||||
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.texture = texture_;
|
||||
dst.mip_level = 0;
|
||||
dst.layer = 0;
|
||||
dst.layer = 0;
|
||||
dst.x = dst.y = dst.z = 0;
|
||||
dst.w = static_cast<Uint32>(w);
|
||||
dst.h = static_cast<Uint32>(h);
|
||||
@@ -185,22 +205,22 @@ bool GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels,
|
||||
SDL_SubmitGPUCommandBuffer(cmd);
|
||||
|
||||
SDL_ReleaseGPUTransferBuffer(device, transfer);
|
||||
width_ = w;
|
||||
width_ = w;
|
||||
height_ = h;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GpuTexture::createSampler(SDL_GPUDevice* device, bool nearest) {
|
||||
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.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_) {
|
||||
if (sampler_ == nullptr) {
|
||||
SDL_Log("GpuTexture: SDL_CreateGPUSampler failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <SDL3/SDL_surface.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
// ============================================================================
|
||||
@@ -9,40 +10,38 @@
|
||||
// Handles sprite textures, render targets, and the 1×1 white utility texture.
|
||||
// ============================================================================
|
||||
class GpuTexture {
|
||||
public:
|
||||
GpuTexture() = default;
|
||||
~GpuTexture() = default;
|
||||
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);
|
||||
// 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);
|
||||
// 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 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);
|
||||
// Create a 1×1 opaque white texture (used for untextured geometry).
|
||||
bool createWhite(SDL_GPUDevice* device);
|
||||
|
||||
// Release GPU resources.
|
||||
void destroy(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; }
|
||||
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);
|
||||
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;
|
||||
SDL_GPUTexture* texture_ = nullptr;
|
||||
SDL_GPUSampler* sampler_ = nullptr;
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
};
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
#include "shader_manager.hpp"
|
||||
|
||||
#include <SDL3/SDL_log.h>
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
void ShaderManager::scan(const std::string& root_dir) {
|
||||
root_dir_ = root_dir;
|
||||
names_.clear();
|
||||
dirs_.clear();
|
||||
|
||||
std::error_code ec;
|
||||
for (const auto& entry : fs::directory_iterator(root_dir, ec)) {
|
||||
if (!entry.is_directory()) continue;
|
||||
fs::path ini = entry.path() / "preset.ini";
|
||||
if (!fs::exists(ini)) continue;
|
||||
|
||||
std::string preset_name = entry.path().filename().string();
|
||||
names_.push_back(preset_name);
|
||||
dirs_[preset_name] = entry.path().string();
|
||||
}
|
||||
|
||||
if (ec) {
|
||||
SDL_Log("ShaderManager: scan error on %s: %s", root_dir.c_str(), ec.message().c_str());
|
||||
}
|
||||
|
||||
std::sort(names_.begin(), names_.end());
|
||||
SDL_Log("ShaderManager: found %d preset(s) in %s", (int)names_.size(), root_dir.c_str());
|
||||
}
|
||||
|
||||
GpuShaderPreset* ShaderManager::load(SDL_GPUDevice* device,
|
||||
const std::string& name,
|
||||
SDL_GPUTextureFormat swapchain_fmt,
|
||||
int w, int h) {
|
||||
auto it = loaded_.find(name);
|
||||
if (it != loaded_.end()) return it->second.get();
|
||||
|
||||
auto dir_it = dirs_.find(name);
|
||||
if (dir_it == dirs_.end()) {
|
||||
SDL_Log("ShaderManager: preset '%s' not found", name.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto preset = std::make_unique<GpuShaderPreset>();
|
||||
if (!preset->load(device, dir_it->second, swapchain_fmt, w, h)) {
|
||||
SDL_Log("ShaderManager: failed to load preset '%s'", name.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
GpuShaderPreset* raw = preset.get();
|
||||
loaded_[name] = std::move(preset);
|
||||
return raw;
|
||||
}
|
||||
|
||||
void ShaderManager::onResize(SDL_GPUDevice* device, SDL_GPUTextureFormat /*swapchain_fmt*/, int w, int h) {
|
||||
for (auto& [name, preset] : loaded_) {
|
||||
preset->recreateTargets(device, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
void ShaderManager::destroyAll(SDL_GPUDevice* device) {
|
||||
for (auto& [name, preset] : loaded_) {
|
||||
preset->destroy(device);
|
||||
}
|
||||
loaded_.clear();
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "gpu_shader_preset.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// ShaderManager — discovers and manages runtime shader presets under
|
||||
// a root directory (e.g., data/shaders/).
|
||||
//
|
||||
// Each subdirectory with a preset.ini is treated as a shader preset.
|
||||
// ============================================================================
|
||||
class ShaderManager {
|
||||
public:
|
||||
// Scan root_dir for preset subdirectories (each must contain preset.ini).
|
||||
void scan(const std::string& root_dir);
|
||||
|
||||
// Available preset names (e.g. {"ntsc-md-rainbows"}).
|
||||
const std::vector<std::string>& names() const { return names_; }
|
||||
|
||||
// Load and return a preset (cached). Returns null on failure.
|
||||
GpuShaderPreset* load(SDL_GPUDevice* device,
|
||||
const std::string& name,
|
||||
SDL_GPUTextureFormat swapchain_fmt,
|
||||
int w, int h);
|
||||
|
||||
// Recreate intermediate render targets on resolution change.
|
||||
void onResize(SDL_GPUDevice* device, SDL_GPUTextureFormat swapchain_fmt, int w, int h);
|
||||
|
||||
void destroyAll(SDL_GPUDevice* device);
|
||||
|
||||
private:
|
||||
std::string root_dir_;
|
||||
std::vector<std::string> names_;
|
||||
std::map<std::string, std::string> dirs_;
|
||||
std::map<std::string, std::unique_ptr<GpuShaderPreset>> loaded_;
|
||||
};
|
||||
@@ -1,13 +1,14 @@
|
||||
#include "input_handler.hpp"
|
||||
|
||||
#include <SDL3/SDL_keycode.h> // for SDL_Keycode
|
||||
#include <string> // for std::string, std::to_string
|
||||
|
||||
#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
|
||||
|
||||
bool InputHandler::processEvents(Engine& engine) {
|
||||
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)
|
||||
@@ -19,7 +20,7 @@ bool InputHandler::processEvents(Engine& engine) {
|
||||
}
|
||||
|
||||
// Procesar eventos de teclado
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat == 0) {
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && static_cast<int>(event.key.repeat) == 0) {
|
||||
switch (event.key.key) {
|
||||
case SDLK_ESCAPE:
|
||||
if (engine.isKioskMode()) {
|
||||
@@ -38,19 +39,19 @@ bool InputHandler::processEvents(Engine& engine) {
|
||||
|
||||
// Controles de dirección de gravedad con teclas de cursor
|
||||
case SDLK_UP:
|
||||
engine.handleGravityDirectionChange(GravityDirection::UP, "Gravedad Arriba");
|
||||
engine.handleGravityDirectionChange(GravityDirection::UP, "Gravedad arriba");
|
||||
break;
|
||||
|
||||
case SDLK_DOWN:
|
||||
engine.handleGravityDirectionChange(GravityDirection::DOWN, "Gravedad Abajo");
|
||||
engine.handleGravityDirectionChange(GravityDirection::DOWN, "Gravedad abajo");
|
||||
break;
|
||||
|
||||
case SDLK_LEFT:
|
||||
engine.handleGravityDirectionChange(GravityDirection::LEFT, "Gravedad Izquierda");
|
||||
engine.handleGravityDirectionChange(GravityDirection::LEFT, "Gravedad izquierda");
|
||||
break;
|
||||
|
||||
case SDLK_RIGHT:
|
||||
engine.handleGravityDirectionChange(GravityDirection::RIGHT, "Gravedad Derecha");
|
||||
engine.handleGravityDirectionChange(GravityDirection::RIGHT, "Gravedad derecha");
|
||||
break;
|
||||
|
||||
case SDLK_V:
|
||||
@@ -105,23 +106,21 @@ bool InputHandler::processEvents(Engine& engine) {
|
||||
|
||||
// Toggle Modo Boids (comportamiento de enjambre)
|
||||
case SDLK_B:
|
||||
engine.toggleBoidsMode();
|
||||
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) {
|
||||
// Shift+C: Ciclar hacia atrás (tema anterior)
|
||||
engine.cycleTheme(false);
|
||||
} else {
|
||||
// C solo: Ciclar hacia adelante (tema siguiente)
|
||||
engine.cycleTheme(true);
|
||||
}
|
||||
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;
|
||||
} break;
|
||||
|
||||
// Temas de colores con teclado numérico (con transición suave)
|
||||
case SDLK_KP_1:
|
||||
@@ -193,65 +192,77 @@ bool InputHandler::processEvents(Engine& engine) {
|
||||
|
||||
// Cambio de número de pelotas (escenarios 1-8)
|
||||
case SDLK_1:
|
||||
engine.changeScenario(0, "10 Pelotas");
|
||||
engine.changeScenario(0, "10 pelotas");
|
||||
break;
|
||||
|
||||
case SDLK_2:
|
||||
engine.changeScenario(1, "50 Pelotas");
|
||||
engine.changeScenario(1, "50 pelotas");
|
||||
break;
|
||||
|
||||
case SDLK_3:
|
||||
engine.changeScenario(2, "100 Pelotas");
|
||||
engine.changeScenario(2, "100 pelotas");
|
||||
break;
|
||||
|
||||
case SDLK_4:
|
||||
engine.changeScenario(3, "500 Pelotas");
|
||||
engine.changeScenario(3, "500 pelotas");
|
||||
break;
|
||||
|
||||
case SDLK_5:
|
||||
engine.changeScenario(4, "1,000 Pelotas");
|
||||
engine.changeScenario(4, "1.000 pelotas");
|
||||
break;
|
||||
|
||||
case SDLK_6:
|
||||
engine.changeScenario(5, "5,000 Pelotas");
|
||||
engine.changeScenario(5, "5.000 pelotas");
|
||||
break;
|
||||
|
||||
case SDLK_7:
|
||||
engine.changeScenario(6, "10,000 Pelotas");
|
||||
engine.changeScenario(6, "10.000 pelotas");
|
||||
break;
|
||||
|
||||
case SDLK_8:
|
||||
engine.changeScenario(7, "50,000 Pelotas");
|
||||
engine.changeScenario(7, "50.000 pelotas");
|
||||
break;
|
||||
|
||||
case SDLK_9:
|
||||
if (engine.isCustomScenarioEnabled()) {
|
||||
std::string custom_notif = std::to_string(engine.getCustomScenarioBalls()) + " Pelotas";
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
if (engine.isKioskMode()) {
|
||||
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
|
||||
} else {
|
||||
engine.toggleRealFullscreen();
|
||||
}
|
||||
break;
|
||||
|
||||
// Toggle PostFX activo/inactivo
|
||||
@@ -264,10 +275,27 @@ bool InputHandler::processEvents(Engine& engine) {
|
||||
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) {
|
||||
if ((event.key.mod & SDL_KMOD_SHIFT) != 0u) {
|
||||
engine.pauseDynamicTheme();
|
||||
} else {
|
||||
// D sin Shift = Toggle DEMO ↔ SANDBOX
|
||||
|
||||
@@ -24,7 +24,7 @@ class InputHandler {
|
||||
* @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
|
||||
*/
|
||||
bool processEvents(Engine& engine);
|
||||
static bool processEvents(Engine& engine);
|
||||
|
||||
private:
|
||||
// Sin estado interno por ahora - el InputHandler es stateless
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include "engine.hpp"
|
||||
|
||||
#include "defines.hpp"
|
||||
#include "engine.hpp"
|
||||
#include "resource_manager.hpp"
|
||||
|
||||
// getExecutableDirectory() ya está definido en defines.h como inline
|
||||
@@ -11,9 +12,9 @@ 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: 320)\n";
|
||||
std::cout << " -h, --height <px> Alto de resolución (default: 240)\n";
|
||||
std::cout << " -z, --zoom <n> Zoom de ventana (default: 3)\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";
|
||||
@@ -24,10 +25,9 @@ void printHelp() {
|
||||
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 << " --shader <nombre> Arrancar con shader externo (ej: ntsc-md-rainbows)\n";
|
||||
std::cout << " --help Mostrar esta ayuda\n\n";
|
||||
std::cout << "Ejemplos:\n";
|
||||
std::cout << " vibe3_physics # 320x240 zoom 3 (ventana 960x720)\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";
|
||||
@@ -39,7 +39,7 @@ void printHelp() {
|
||||
std::cout << "Nota: Si resolución > pantalla, se usa default. Zoom se ajusta automáticamente.\n";
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
auto main(int argc, char* argv[]) -> int { // NOLINT(readability-function-cognitive-complexity)
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int zoom = 0;
|
||||
@@ -52,7 +52,6 @@ int main(int argc, char* argv[]) {
|
||||
int initial_postfx = -1;
|
||||
float override_vignette = -1.f;
|
||||
float override_chroma = -1.f;
|
||||
std::string initial_shader;
|
||||
AppMode initial_mode = AppMode::SANDBOX; // Modo inicial (default: SANDBOX)
|
||||
|
||||
// Parsear argumentos
|
||||
@@ -60,7 +59,8 @@ int main(int argc, char* argv[]) {
|
||||
if (strcmp(argv[i], "--help") == 0) {
|
||||
printHelp();
|
||||
return 0;
|
||||
} else if (strcmp(argv[i], "-w") == 0 || strcmp(argv[i], "--width") == 0) {
|
||||
}
|
||||
if (strcmp(argv[i], "-w") == 0 || strcmp(argv[i], "--width") == 0) {
|
||||
if (i + 1 < argc) {
|
||||
width = atoi(argv[++i]);
|
||||
if (width < 320) {
|
||||
@@ -177,13 +177,6 @@ int main(int argc, char* argv[]) {
|
||||
std::cerr << "Error: --max-balls requiere un valor\n";
|
||||
return -1;
|
||||
}
|
||||
} else if (strcmp(argv[i], "--shader") == 0) {
|
||||
if (i + 1 < argc) {
|
||||
initial_shader = argv[++i];
|
||||
} else {
|
||||
std::cerr << "Error: --shader requiere un nombre de preset\n";
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
std::cerr << "Error: Opción desconocida '" << argv[i] << "'\n";
|
||||
printHelp();
|
||||
@@ -198,28 +191,29 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
Engine engine;
|
||||
|
||||
if (custom_balls > 0)
|
||||
if (custom_balls > 0) {
|
||||
engine.setCustomScenario(custom_balls); // pre-init: asigna campos antes del benchmark
|
||||
}
|
||||
|
||||
if (max_balls_override > 0)
|
||||
if (max_balls_override > 0) {
|
||||
engine.setMaxBallsOverride(max_balls_override);
|
||||
else if (skip_benchmark)
|
||||
} else if (skip_benchmark) {
|
||||
engine.setSkipBenchmark();
|
||||
}
|
||||
|
||||
if (initial_postfx >= 0)
|
||||
if (initial_postfx >= 0) {
|
||||
engine.setInitialPostFX(initial_postfx);
|
||||
}
|
||||
|
||||
if (override_vignette >= 0.f || override_chroma >= 0.f) {
|
||||
if (initial_postfx < 0)
|
||||
if (initial_postfx < 0) {
|
||||
engine.setInitialPostFX(0);
|
||||
}
|
||||
engine.setPostFXParamOverrides(override_vignette, override_chroma);
|
||||
}
|
||||
|
||||
if (!initial_shader.empty())
|
||||
engine.setInitialShader(initial_shader);
|
||||
|
||||
if (!engine.initialize(width, height, zoom, fullscreen, initial_mode)) {
|
||||
std::cout << "¡Error al inicializar el engine!" << std::endl;
|
||||
std::cout << "¡Error al inicializar el engine!" << '\n';
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
#include "resource_manager.hpp"
|
||||
#include "resource_pack.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#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_;
|
||||
|
||||
bool ResourceManager::init(const std::string& packFilePath) {
|
||||
auto ResourceManager::init(const std::string& pack_file_path) -> bool {
|
||||
// Si ya estaba inicializado, liberar primero
|
||||
if (resourcePack_ != nullptr) {
|
||||
delete resourcePack_;
|
||||
@@ -18,15 +19,15 @@ bool ResourceManager::init(const std::string& packFilePath) {
|
||||
|
||||
// Intentar cargar el pack
|
||||
resourcePack_ = new ResourcePack();
|
||||
if (!resourcePack_->loadPack(packFilePath)) {
|
||||
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/" << std::endl;
|
||||
std::cout << "resources.pack no encontrado - usando carpeta data/" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "resources.pack cargado (" << resourcePack_->getResourceCount() << " recursos)" << std::endl;
|
||||
std::cout << "resources.pack cargado (" << resourcePack_->getResourceCount() << " recursos)" << '\n';
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -38,12 +39,12 @@ void ResourceManager::shutdown() {
|
||||
}
|
||||
}
|
||||
|
||||
bool ResourceManager::loadResource(const std::string& resourcePath, unsigned char*& data, size_t& size) {
|
||||
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(resourcePath);
|
||||
auto it = cache_.find(resource_path);
|
||||
if (it != cache_.end()) {
|
||||
size = it->second.size();
|
||||
data = new unsigned char[size];
|
||||
@@ -53,20 +54,20 @@ bool ResourceManager::loadResource(const std::string& resourcePath, unsigned cha
|
||||
|
||||
// 2. Intentar cargar desde pack (si está disponible)
|
||||
if (resourcePack_ != nullptr) {
|
||||
ResourcePack::ResourceData packData = resourcePack_->loadResource(resourcePath);
|
||||
if (packData.data != nullptr) {
|
||||
cache_[resourcePath] = std::vector<unsigned char>(packData.data, packData.data + packData.size);
|
||||
data = packData.data;
|
||||
size = packData.size;
|
||||
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(resourcePath, std::ios::binary | std::ios::ate);
|
||||
std::ifstream file(resource_path, std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
std::string dataPath = "data/" + resourcePath;
|
||||
file.open(dataPath, std::ios::binary | std::ios::ate);
|
||||
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());
|
||||
@@ -76,22 +77,22 @@ bool ResourceManager::loadResource(const std::string& resourcePath, unsigned cha
|
||||
file.close();
|
||||
|
||||
// Guardar en caché
|
||||
cache_[resourcePath] = std::vector<unsigned char>(data, data + size);
|
||||
cache_[resource_path] = std::vector<unsigned char>(data, data + size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ResourceManager::isPackLoaded() {
|
||||
auto ResourceManager::isPackLoaded() -> bool {
|
||||
return resourcePack_ != nullptr;
|
||||
}
|
||||
|
||||
std::vector<std::string> ResourceManager::getResourceList() {
|
||||
auto ResourceManager::getResourceList() -> std::vector<std::string> {
|
||||
if (resourcePack_ != nullptr) {
|
||||
return resourcePack_->getResourceList();
|
||||
}
|
||||
return std::vector<std::string>(); // Vacío si no hay pack
|
||||
return {}; // Vacío si no hay pack
|
||||
}
|
||||
|
||||
size_t ResourceManager::getResourceCount() {
|
||||
auto ResourceManager::getResourceCount() -> size_t {
|
||||
if (resourcePack_ != nullptr) {
|
||||
return resourcePack_->getResourceCount();
|
||||
}
|
||||
|
||||
@@ -25,62 +25,62 @@ class ResourcePack;
|
||||
* }
|
||||
*/
|
||||
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& packFilePath);
|
||||
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();
|
||||
/**
|
||||
* 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& resourcePath, unsigned char*& data, size_t& size);
|
||||
/**
|
||||
* 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();
|
||||
/**
|
||||
* 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 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();
|
||||
/**
|
||||
* 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;
|
||||
private:
|
||||
// Constructor privado (singleton)
|
||||
ResourceManager() = default;
|
||||
~ResourceManager() = default;
|
||||
|
||||
// Deshabilitar copia y asignación
|
||||
ResourceManager(const ResourceManager&) = delete;
|
||||
ResourceManager& operator=(const ResourceManager&) = delete;
|
||||
// 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_;
|
||||
// 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_;
|
||||
// Caché en RAM para evitar I/O repetido en el bucle principal
|
||||
static std::map<std::string, std::vector<unsigned char>> cache_;
|
||||
};
|
||||
|
||||
@@ -7,10 +7,8 @@
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Clave XOR para ofuscación simple (puede cambiarse)
|
||||
constexpr uint8_t XOR_KEY = 0x5A;
|
||||
|
||||
ResourcePack::ResourcePack() : isLoaded_(false) {}
|
||||
ResourcePack::ResourcePack()
|
||||
: isLoaded_(false) {}
|
||||
|
||||
ResourcePack::~ResourcePack() {
|
||||
clear();
|
||||
@@ -20,54 +18,54 @@ ResourcePack::~ResourcePack() {
|
||||
// EMPAQUETADO (herramienta pack_resources)
|
||||
// ============================================================================
|
||||
|
||||
bool ResourcePack::addDirectory(const std::string& dirPath, const std::string& prefix) {
|
||||
if (!fs::exists(dirPath) || !fs::is_directory(dirPath)) {
|
||||
std::cerr << "Error: Directorio no existe: " << dirPath << std::endl;
|
||||
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(dirPath)) {
|
||||
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 relativePath = fs::relative(entry.path(), dirPath).string();
|
||||
std::string fullPath = prefix.empty() ? relativePath : prefix + "/" + relativePath;
|
||||
fullPath = normalizePath(fullPath);
|
||||
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() << std::endl;
|
||||
std::cerr << "Error: No se pudo abrir: " << entry.path() << '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
file.seekg(0, std::ios::end);
|
||||
size_t fileSize = file.tellg();
|
||||
size_t file_size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
std::vector<unsigned char> buffer(fileSize);
|
||||
file.read(reinterpret_cast<char*>(buffer.data()), fileSize);
|
||||
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 = fullPath;
|
||||
resource.path = full_path;
|
||||
resource.offset = 0; // Se calculará al guardar
|
||||
resource.size = static_cast<uint32_t>(fileSize);
|
||||
resource.checksum = calculateChecksum(buffer.data(), fileSize);
|
||||
resource.size = static_cast<uint32_t>(file_size);
|
||||
resource.checksum = calculateChecksum(buffer.data(), file_size);
|
||||
|
||||
resources_[fullPath] = resource;
|
||||
resources_[full_path] = resource;
|
||||
|
||||
std::cout << " Añadido: " << fullPath << " (" << fileSize << " bytes)" << std::endl;
|
||||
std::cout << " Añadido: " << full_path << " (" << file_size << " bytes)" << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
return !resources_.empty();
|
||||
}
|
||||
|
||||
bool ResourcePack::savePack(const std::string& packFilePath) {
|
||||
std::ofstream packFile(packFilePath, std::ios::binary);
|
||||
if (!packFile) {
|
||||
std::cerr << "Error: No se pudo crear pack: " << packFilePath << std::endl;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -76,39 +74,39 @@ bool ResourcePack::savePack(const std::string& packFilePath) {
|
||||
std::memcpy(header.magic, "VBE3", 4);
|
||||
header.version = 1;
|
||||
header.fileCount = static_cast<uint32_t>(resources_.size());
|
||||
packFile.write(reinterpret_cast<const char*>(&header), sizeof(PackHeader));
|
||||
pack_file.write(reinterpret_cast<const char*>(&header), sizeof(PackHeader));
|
||||
|
||||
// 2. Calcular offsets (después del header + índice)
|
||||
uint32_t currentOffset = sizeof(PackHeader);
|
||||
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_) {
|
||||
currentOffset += sizeof(uint32_t); // pathLen
|
||||
currentOffset += static_cast<uint32_t>(path.size()); // path
|
||||
currentOffset += sizeof(uint32_t) * 3; // offset, size, checksum
|
||||
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 = currentOffset;
|
||||
entry.offset = current_offset;
|
||||
|
||||
uint32_t pathLen = static_cast<uint32_t>(path.size());
|
||||
packFile.write(reinterpret_cast<const char*>(&pathLen), sizeof(uint32_t));
|
||||
packFile.write(path.c_str(), pathLen);
|
||||
packFile.write(reinterpret_cast<const char*>(&entry.offset), sizeof(uint32_t));
|
||||
packFile.write(reinterpret_cast<const char*>(&entry.size), sizeof(uint32_t));
|
||||
packFile.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(uint32_t));
|
||||
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));
|
||||
|
||||
currentOffset += entry.size;
|
||||
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 originalPath = fs::current_path() / "data" / path;
|
||||
std::ifstream file(originalPath, std::ios::binary);
|
||||
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: " << originalPath << std::endl;
|
||||
std::cerr << "Error: No se pudo re-leer: " << original_path << '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -116,10 +114,10 @@ bool ResourcePack::savePack(const std::string& packFilePath) {
|
||||
file.read(reinterpret_cast<char*>(buffer.data()), entry.size);
|
||||
file.close();
|
||||
|
||||
packFile.write(reinterpret_cast<const char*>(buffer.data()), entry.size);
|
||||
pack_file.write(reinterpret_cast<const char*>(buffer.data()), entry.size);
|
||||
}
|
||||
|
||||
packFile.close();
|
||||
pack_file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -127,10 +125,10 @@ bool ResourcePack::savePack(const std::string& packFilePath) {
|
||||
// DESEMPAQUETADO (juego)
|
||||
// ============================================================================
|
||||
|
||||
bool ResourcePack::loadPack(const std::string& packFilePath) {
|
||||
auto ResourcePack::loadPack(const std::string& pack_file_path) -> bool {
|
||||
clear();
|
||||
|
||||
packFile_.open(packFilePath, std::ios::binary);
|
||||
packFile_.open(pack_file_path, std::ios::binary);
|
||||
if (!packFile_) {
|
||||
return false;
|
||||
}
|
||||
@@ -140,13 +138,13 @@ bool ResourcePack::loadPack(const std::string& packFilePath) {
|
||||
packFile_.read(reinterpret_cast<char*>(&header), sizeof(PackHeader));
|
||||
|
||||
if (std::memcmp(header.magic, "VBE3", 4) != 0) {
|
||||
std::cerr << "Error: Pack inválido (magic incorrecto)" << std::endl;
|
||||
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 << std::endl;
|
||||
std::cerr << "Error: Versión de pack no soportada: " << header.version << '\n';
|
||||
packFile_.close();
|
||||
return false;
|
||||
}
|
||||
@@ -155,12 +153,12 @@ bool ResourcePack::loadPack(const std::string& packFilePath) {
|
||||
for (uint32_t i = 0; i < header.fileCount; i++) {
|
||||
ResourceEntry entry;
|
||||
|
||||
uint32_t pathLen;
|
||||
packFile_.read(reinterpret_cast<char*>(&pathLen), sizeof(uint32_t));
|
||||
uint32_t path_len;
|
||||
packFile_.read(reinterpret_cast<char*>(&path_len), sizeof(uint32_t));
|
||||
|
||||
std::vector<char> pathBuffer(pathLen + 1, '\0');
|
||||
packFile_.read(pathBuffer.data(), pathLen);
|
||||
entry.path = std::string(pathBuffer.data());
|
||||
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));
|
||||
@@ -173,15 +171,15 @@ bool ResourcePack::loadPack(const std::string& packFilePath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ResourcePack::ResourceData ResourcePack::loadResource(const std::string& resourcePath) {
|
||||
ResourceData result = {nullptr, 0};
|
||||
auto ResourcePack::loadResource(const std::string& resource_path) -> ResourcePack::ResourceData {
|
||||
ResourceData result = {.data = nullptr, .size = 0};
|
||||
|
||||
if (!isLoaded_) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string normalizedPath = normalizePath(resourcePath);
|
||||
auto it = resources_.find(normalizedPath);
|
||||
std::string normalized_path = normalizePath(resource_path);
|
||||
auto it = resources_.find(normalized_path);
|
||||
if (it == resources_.end()) {
|
||||
return result;
|
||||
}
|
||||
@@ -197,7 +195,7 @@ ResourcePack::ResourceData ResourcePack::loadResource(const std::string& resourc
|
||||
// Verificar checksum
|
||||
uint32_t checksum = calculateChecksum(result.data, entry.size);
|
||||
if (checksum != entry.checksum) {
|
||||
std::cerr << "Warning: Checksum incorrecto para: " << resourcePath << std::endl;
|
||||
std::cerr << "Warning: Checksum incorrecto para: " << resource_path << '\n';
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -207,15 +205,16 @@ ResourcePack::ResourceData ResourcePack::loadResource(const std::string& resourc
|
||||
// UTILIDADES
|
||||
// ============================================================================
|
||||
|
||||
std::vector<std::string> ResourcePack::getResourceList() const {
|
||||
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;
|
||||
}
|
||||
|
||||
size_t ResourcePack::getResourceCount() const {
|
||||
auto ResourcePack::getResourceCount() const -> size_t {
|
||||
return resources_.size();
|
||||
}
|
||||
|
||||
@@ -231,18 +230,7 @@ void ResourcePack::clear() {
|
||||
// FUNCIONES AUXILIARES
|
||||
// ============================================================================
|
||||
|
||||
void ResourcePack::encryptData(unsigned char* data, size_t size) {
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
data[i] ^= XOR_KEY;
|
||||
}
|
||||
}
|
||||
|
||||
void ResourcePack::decryptData(unsigned char* data, size_t size) {
|
||||
// XOR es simétrico
|
||||
encryptData(data, size);
|
||||
}
|
||||
|
||||
uint32_t ResourcePack::calculateChecksum(const unsigned char* data, size_t size) {
|
||||
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]);
|
||||
@@ -251,11 +239,11 @@ uint32_t ResourcePack::calculateChecksum(const unsigned char* data, size_t size)
|
||||
return checksum;
|
||||
}
|
||||
|
||||
std::string ResourcePack::normalizePath(const std::string& path) {
|
||||
auto ResourcePack::normalizePath(const std::string& path) -> std::string {
|
||||
std::string normalized = path;
|
||||
|
||||
// Reemplazar \ por /
|
||||
std::replace(normalized.begin(), normalized.end(), '\\', '/');
|
||||
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/");
|
||||
|
||||
@@ -13,53 +13,51 @@
|
||||
* único y ofuscado. Proporciona fallback automático a carpeta data/ si no existe pack.
|
||||
*/
|
||||
class ResourcePack {
|
||||
public:
|
||||
ResourcePack();
|
||||
~ResourcePack();
|
||||
public:
|
||||
ResourcePack();
|
||||
~ResourcePack();
|
||||
|
||||
// Empaquetado (usado por herramienta pack_resources)
|
||||
bool addDirectory(const std::string& dirPath, const std::string& prefix = "");
|
||||
bool savePack(const std::string& packFilePath);
|
||||
// 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& packFilePath);
|
||||
// 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& resourcePath);
|
||||
// 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();
|
||||
// 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
|
||||
};
|
||||
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)
|
||||
};
|
||||
// Í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_;
|
||||
// Datos internos
|
||||
std::map<std::string, ResourceEntry> resources_;
|
||||
std::ifstream packFile_;
|
||||
bool isLoaded_;
|
||||
|
||||
// Funciones auxiliares
|
||||
void encryptData(unsigned char* data, size_t size);
|
||||
void decryptData(unsigned char* data, size_t size);
|
||||
uint32_t calculateChecksum(const unsigned char* data, size_t size);
|
||||
std::string normalizePath(const std::string& path);
|
||||
// Funciones auxiliares
|
||||
static uint32_t calculateChecksum(const unsigned char* data, size_t size);
|
||||
static std::string normalizePath(const std::string& path);
|
||||
};
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
#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
|
||||
#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) {
|
||||
: 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_ = texture;
|
||||
texture_ = std::move(texture);
|
||||
theme_manager_ = theme_manager;
|
||||
current_ball_size_ = texture_->getWidth();
|
||||
|
||||
@@ -48,28 +49,31 @@ void SceneManager::changeScenario(int scenario_id, SimulationMode mode) {
|
||||
? custom_ball_count_
|
||||
: BALL_COUNT_SCENARIOS[scenario_id];
|
||||
for (int i = 0; i < ball_count; ++i) {
|
||||
float X, Y, VX, VY;
|
||||
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;
|
||||
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;
|
||||
x = screen_width_ / 2.0f;
|
||||
y = screen_height_ / 2.0f; // Centro vertical
|
||||
vx = 0.0f;
|
||||
vy = 0.0f;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -77,48 +81,57 @@ void SceneManager::changeScenario(int scenario_id, SimulationMode mode) {
|
||||
// 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;
|
||||
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;
|
||||
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);
|
||||
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);
|
||||
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
|
||||
));
|
||||
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;
|
||||
if (balls_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Guardar tamaño antiguo
|
||||
int old_size = current_ball_size_;
|
||||
|
||||
// Actualizar textura y tamaño
|
||||
texture_ = new_texture;
|
||||
texture_ = std::move(new_texture);
|
||||
current_ball_size_ = new_ball_size;
|
||||
|
||||
// Actualizar texturas de todas las pelotas
|
||||
@@ -136,7 +149,8 @@ void SceneManager::pushBallsAwayFromGravity() {
|
||||
const float LATERAL = (((rand() % 20) + 10) * 0.1f) * SIGNO;
|
||||
const float MAIN = ((rand() % 40) * 0.1f) + 5;
|
||||
|
||||
float vx = 0, vy = 0;
|
||||
float vx = 0;
|
||||
float vy = 0;
|
||||
switch (current_gravity_) {
|
||||
case GravityDirection::DOWN: // Impulsar ARRIBA
|
||||
vx = LATERAL;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#include "atom_shape.hpp"
|
||||
#include "defines.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;
|
||||
@@ -25,15 +28,15 @@ 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
|
||||
if (nucleus_points < 1) nucleus_points = 1;
|
||||
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 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);
|
||||
@@ -51,16 +54,18 @@ void AtomShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
// Puntos restantes: distribuir en órbitas
|
||||
int orbit_points = num_points_ - nucleus_points;
|
||||
int points_per_orbit = orbit_points / num_orbits;
|
||||
if (points_per_orbit < 1) points_per_orbit = 1;
|
||||
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;
|
||||
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
|
||||
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;
|
||||
@@ -73,21 +78,21 @@ void AtomShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
// 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;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
float AtomShape::getScaleFactor(float screen_height) const {
|
||||
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;
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
// 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
|
||||
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;
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#include "cube_shape.hpp"
|
||||
#include "defines.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;
|
||||
@@ -52,23 +55,23 @@ void CubeShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
// 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 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 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;
|
||||
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;
|
||||
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;
|
||||
@@ -76,7 +79,7 @@ void CubeShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
z = z_final;
|
||||
}
|
||||
|
||||
float CubeShape::getScaleFactor(float screen_height) const {
|
||||
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;
|
||||
@@ -105,12 +108,24 @@ void CubeShape::generateVerticesAndCenters() {
|
||||
|
||||
// 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
|
||||
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)
|
||||
@@ -143,16 +158,16 @@ void CubeShape::generateVerticesAndCenters() {
|
||||
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_))));
|
||||
if (grid_dim < 3) grid_dim = 3; // Mínimo grid 3x3x3
|
||||
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;
|
||||
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);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "shape.hpp"
|
||||
#include <vector>
|
||||
|
||||
#include "shape.hpp"
|
||||
|
||||
// Figura: Cubo 3D rotante
|
||||
// Distribución:
|
||||
// - 1-8 pelotas: Solo vértices (8 puntos)
|
||||
@@ -10,28 +11,28 @@
|
||||
// - 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
|
||||
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_;
|
||||
// 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;
|
||||
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
|
||||
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
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#include "cylinder_shape.hpp"
|
||||
#include "defines.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;
|
||||
@@ -37,7 +40,7 @@ void CylinderShape::update(float delta_time, float screen_width, float screen_he
|
||||
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;
|
||||
: 1.0f - ((-2.0f * t + 2.0f) * (-2.0f * t + 2.0f) / 2.0f);
|
||||
angle_x_ = ease * tumble_target_;
|
||||
}
|
||||
} else {
|
||||
@@ -58,10 +61,10 @@ void CylinderShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
// 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));
|
||||
if (num_rings < 2) num_rings = 2;
|
||||
num_rings = std::max(num_rings, 2);
|
||||
|
||||
int points_per_ring = num_points_ / num_rings;
|
||||
if (points_per_ring < 3) points_per_ring = 3;
|
||||
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;
|
||||
@@ -80,8 +83,10 @@ void CylinderShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
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;
|
||||
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)
|
||||
@@ -94,14 +99,14 @@ void CylinderShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
// 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;
|
||||
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;
|
||||
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;
|
||||
@@ -109,7 +114,7 @@ void CylinderShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
z = z_rot;
|
||||
}
|
||||
|
||||
float CylinderShape::getScaleFactor(float screen_height) const {
|
||||
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;
|
||||
|
||||
@@ -6,23 +6,23 @@
|
||||
// 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
|
||||
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
|
||||
// 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;
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "helix_shape.hpp"
|
||||
#include "defines.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;
|
||||
@@ -41,8 +43,8 @@ void HelixShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
// 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;
|
||||
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;
|
||||
@@ -50,7 +52,7 @@ void HelixShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
z = z_rot;
|
||||
}
|
||||
|
||||
float HelixShape::getScaleFactor(float screen_height) const {
|
||||
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;
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
// 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
|
||||
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;
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#include "icosahedron_shape.hpp"
|
||||
#include "defines.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;
|
||||
@@ -21,37 +25,36 @@ void IcosahedronShape::update(float delta_time, float screen_width, float screen
|
||||
|
||||
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;
|
||||
const float PHI = (1.0f + sqrtf(5.0f)) / 2.0f;
|
||||
|
||||
// 12 vértices del icosaedro regular normalizado
|
||||
// Basados en 3 rectángulos áureos ortogonales
|
||||
static const float vertices[12][3] = {
|
||||
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},
|
||||
{-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},
|
||||
{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}
|
||||
};
|
||||
{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);
|
||||
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_;
|
||||
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);
|
||||
@@ -62,9 +65,9 @@ void IcosahedronShape::getPoint3D(int index, float& x, float& y, float& z) const
|
||||
// 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_;
|
||||
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;
|
||||
}
|
||||
@@ -73,38 +76,55 @@ void IcosahedronShape::getPoint3D(int index, float& x, float& y, float& z) const
|
||||
// El icosaedro tiene 20 caras triangulares
|
||||
int remaining_points = index - 12;
|
||||
int points_per_face = (num_points_ - 12) / 20;
|
||||
if (points_per_face < 1) points_per_face = 1;
|
||||
points_per_face = std::max(points_per_face, 1);
|
||||
|
||||
int face_index = remaining_points / points_per_face;
|
||||
if (face_index >= 20) face_index = 19;
|
||||
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 const int faces[20][3] = {
|
||||
{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}
|
||||
};
|
||||
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];
|
||||
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;
|
||||
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);
|
||||
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;
|
||||
@@ -122,27 +142,27 @@ void IcosahedronShape::applyRotations(float x_in, float y_in, float z_in, float&
|
||||
// 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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
float IcosahedronShape::getScaleFactor(float screen_height) const {
|
||||
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;
|
||||
|
||||
@@ -6,20 +6,20 @@
|
||||
// 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
|
||||
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;
|
||||
// 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;
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "lissajous_shape.hpp"
|
||||
#include "defines.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;
|
||||
@@ -33,21 +35,21 @@ void LissajousShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
// 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 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_);
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
@@ -55,7 +57,7 @@ void LissajousShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
z = z_final;
|
||||
}
|
||||
|
||||
float LissajousShape::getScaleFactor(float screen_height) const {
|
||||
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;
|
||||
|
||||
@@ -6,21 +6,21 @@
|
||||
// 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
|
||||
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;
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
#include "png_shape.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
|
||||
#include "defines.hpp"
|
||||
#include "external/stb_image.h"
|
||||
#include "resource_manager.hpp"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
|
||||
PNGShape::PNGShape(const char* png_path) {
|
||||
// Cargar PNG desde path
|
||||
if (!loadPNG(png_path)) {
|
||||
std::cerr << "[PNGShape] Usando fallback 10x10" << std::endl;
|
||||
std::cerr << "[PNGShape] Usando fallback 10x10" << '\n';
|
||||
// Fallback: generar un cuadrado simple si falla la carga
|
||||
image_width_ = 10;
|
||||
image_height_ = 10;
|
||||
@@ -21,20 +23,25 @@ PNGShape::PNGShape(const char* png_path) {
|
||||
next_idle_time_ = PNG_IDLE_TIME_MIN + (rand() % 1000) / 1000.0f * (PNG_IDLE_TIME_MAX - PNG_IDLE_TIME_MIN);
|
||||
}
|
||||
|
||||
bool PNGShape::loadPNG(const char* resource_key) {
|
||||
std::cout << "[PNGShape] Cargando recurso: " << resource_key << std::endl;
|
||||
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 << std::endl;
|
||||
std::cerr << "[PNGShape] ERROR: recurso no encontrado: " << resource_key << '\n';
|
||||
return false;
|
||||
}
|
||||
int width, height, channels;
|
||||
unsigned char* pixels = stbi_load_from_memory(file_data, static_cast<int>(file_size),
|
||||
&width, &height, &channels, 1);
|
||||
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) {
|
||||
std::cerr << "[PNGShape] ERROR al decodificar PNG: " << stbi_failure_reason() << std::endl;
|
||||
if (pixels == nullptr) {
|
||||
std::cerr << "[PNGShape] ERROR al decodificar PNG: " << stbi_failure_reason() << '\n';
|
||||
return false;
|
||||
}
|
||||
image_width_ = width;
|
||||
@@ -53,9 +60,11 @@ void PNGShape::detectEdges() {
|
||||
// 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;
|
||||
int idx = (y * image_width_) + x;
|
||||
|
||||
if (!pixel_data_[idx]) continue; // Solo píxeles blancos
|
||||
if (!pixel_data_[idx]) {
|
||||
continue; // Solo píxeles blancos
|
||||
}
|
||||
|
||||
// Verificar vecinos (arriba, abajo, izq, der)
|
||||
bool is_edge = false;
|
||||
@@ -64,10 +73,10 @@ void PNGShape::detectEdges() {
|
||||
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
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -86,7 +95,7 @@ void PNGShape::floodFill() {
|
||||
|
||||
for (int y = 0; y < image_height_; y++) {
|
||||
for (int x = 0; x < image_width_; x++) {
|
||||
int idx = y * 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)});
|
||||
}
|
||||
@@ -110,8 +119,8 @@ void PNGShape::generatePoints(int num_points, float screen_width, float screen_h
|
||||
num_layers_ = PNG_NUM_EXTRUSION_LAYERS;
|
||||
|
||||
// Generar AMBOS conjuntos de puntos (relleno Y bordes)
|
||||
floodFill(); // Generar filled_points_
|
||||
detectEdges(); // Generar edge_points_
|
||||
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_;
|
||||
@@ -119,7 +128,7 @@ void PNGShape::generatePoints(int num_points, float screen_width, float screen_h
|
||||
|
||||
// Conjunto de puntos ACTIVO (será modificado por filtros)
|
||||
std::vector<Point2D> active_points_data;
|
||||
std::string mode_name = "";
|
||||
std::string mode_name;
|
||||
|
||||
// === SISTEMA DE DISTRIBUCIÓN ADAPTATIVA ===
|
||||
// Estrategia: Optimizar según número de pelotas disponibles
|
||||
@@ -192,8 +201,6 @@ void PNGShape::generatePoints(int num_points, float screen_width, float screen_h
|
||||
std::vector<Point2D> vertices = extractCornerVertices(source_for_vertices);
|
||||
if (!vertices.empty() && vertices.size() < active_points_data.size()) {
|
||||
active_points_data = vertices;
|
||||
num_2d_points = active_points_data.size();
|
||||
total_3d_points = num_2d_points * num_layers_;
|
||||
mode_name = "VÉRTICES";
|
||||
}
|
||||
}
|
||||
@@ -212,7 +219,7 @@ void PNGShape::generatePoints(int num_points, float screen_width, float screen_h
|
||||
|
||||
// Extraer filas alternas de puntos (FUNCIÓN PURA: no modifica parámetros)
|
||||
// Recibe vector original y devuelve nuevo vector filtrado
|
||||
std::vector<PNGShape::Point2D> PNGShape::extractAlternateRows(const std::vector<Point2D>& source, int row_skip) {
|
||||
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()) {
|
||||
@@ -239,7 +246,7 @@ std::vector<PNGShape::Point2D> PNGShape::extractAlternateRows(const std::vector<
|
||||
}
|
||||
|
||||
// Extraer vértices y esquinas (FUNCIÓN PURA: devuelve nuevo vector)
|
||||
std::vector<PNGShape::Point2D> PNGShape::extractCornerVertices(const std::vector<Point2D>& source) {
|
||||
auto PNGShape::extractCornerVertices(const std::vector<Point2D>& source) -> std::vector<PNGShape::Point2D> {
|
||||
std::vector<Point2D> result;
|
||||
|
||||
if (source.empty()) {
|
||||
@@ -263,9 +270,9 @@ std::vector<PNGShape::Point2D> PNGShape::extractCornerVertices(const std::vector
|
||||
|
||||
// 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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,8 +379,8 @@ void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
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%
|
||||
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;
|
||||
@@ -382,14 +389,14 @@ void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
// 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;
|
||||
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;
|
||||
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;
|
||||
@@ -404,7 +411,7 @@ void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
}
|
||||
}
|
||||
|
||||
float PNGShape::getScaleFactor(float screen_height) const {
|
||||
auto PNGShape::getScaleFactor(float screen_height) const -> float {
|
||||
// Escala dinámica según resolución
|
||||
return PNG_SIZE_FACTOR;
|
||||
}
|
||||
@@ -428,7 +435,7 @@ void PNGShape::setConvergence(float convergence) {
|
||||
}
|
||||
|
||||
// Obtener progreso del flip actual (0.0 = inicio del flip, 1.0 = fin del flip)
|
||||
float PNGShape::getFlipProgress() const {
|
||||
auto PNGShape::getFlipProgress() const -> float {
|
||||
if (!is_flipping_) {
|
||||
return 0.0f; // No está flipping, progreso = 0
|
||||
}
|
||||
|
||||
@@ -1,104 +1,108 @@
|
||||
#pragma once
|
||||
|
||||
#include "shape.hpp"
|
||||
#include "defines.hpp" // Para PNG_IDLE_TIME_MIN/MAX constantes
|
||||
#include <cstdlib> // Para rand()
|
||||
#include <vector>
|
||||
#include <cstdlib> // Para rand()
|
||||
|
||||
#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)
|
||||
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)
|
||||
// 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)
|
||||
// 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)
|
||||
// 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
|
||||
// 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)
|
||||
// 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 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
|
||||
// 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;
|
||||
// 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)
|
||||
int num_points_ = 0; // Total de puntos generados (para indexación)
|
||||
|
||||
// Métodos internos
|
||||
bool loadPNG(const char* path); // 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 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)
|
||||
std::vector<Point2D> extractAlternateRows(const std::vector<Point2D>& source, int row_skip); // Extraer filas alternas
|
||||
std::vector<Point2D> extractCornerVertices(const std::vector<Point2D>& source); // Extraer vértices/esquinas
|
||||
// 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");
|
||||
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;
|
||||
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_; }
|
||||
// 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 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_; }
|
||||
// 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; }
|
||||
// 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);
|
||||
}
|
||||
// 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;
|
||||
// Sistema de convergencia (override de Shape::setConvergence)
|
||||
void setConvergence(float convergence) override;
|
||||
};
|
||||
|
||||
@@ -2,34 +2,34 @@
|
||||
|
||||
// Interfaz abstracta para todas las figuras 3D
|
||||
class Shape {
|
||||
public:
|
||||
virtual ~Shape() = default;
|
||||
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;
|
||||
// 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;
|
||||
// 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 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 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;
|
||||
// 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) {}
|
||||
// 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) {}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "sphere_shape.hpp"
|
||||
#include "defines.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;
|
||||
@@ -19,12 +21,12 @@ void SphereShape::update(float delta_time, float screen_width, float screen_heig
|
||||
|
||||
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;
|
||||
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
|
||||
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_;
|
||||
@@ -34,14 +36,14 @@ void SphereShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
// 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;
|
||||
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;
|
||||
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;
|
||||
@@ -49,7 +51,7 @@ void SphereShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
z = z_final;
|
||||
}
|
||||
|
||||
float SphereShape::getScaleFactor(float screen_height) const {
|
||||
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;
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
// 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
|
||||
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;
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#include "torus_shape.hpp"
|
||||
#include "defines.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;
|
||||
@@ -26,10 +29,10 @@ void TorusShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
|
||||
// Calcular número aproximado de anillos y puntos por anillo
|
||||
int num_rings = static_cast<int>(sqrtf(static_cast<float>(num_points_) * 0.5f));
|
||||
if (num_rings < 2) num_rings = 2;
|
||||
num_rings = std::max(num_rings, 2);
|
||||
|
||||
int points_per_ring = num_points_ / num_rings;
|
||||
if (points_per_ring < 3) points_per_ring = 3;
|
||||
points_per_ring = std::max(points_per_ring, 3);
|
||||
|
||||
// Obtener parámetros u y v del índice
|
||||
int ring = index / points_per_ring;
|
||||
@@ -57,7 +60,7 @@ void TorusShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
float cos_u = cosf(u);
|
||||
float sin_u = sinf(u);
|
||||
|
||||
float radius_at_v = major_radius_ + minor_radius_ * cos_v;
|
||||
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;
|
||||
@@ -66,20 +69,20 @@ void TorusShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
// 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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
@@ -87,7 +90,7 @@ void TorusShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
||||
z = z_rot_y;
|
||||
}
|
||||
|
||||
float TorusShape::getScaleFactor(float screen_height) const {
|
||||
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;
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
// 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
|
||||
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;
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
#include "shape_manager.hpp"
|
||||
|
||||
#include <algorithm> // for std::min, std::max
|
||||
#include <cstdlib> // for rand
|
||||
#include <string> // for std::string
|
||||
#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
|
||||
#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"
|
||||
@@ -22,26 +23,24 @@
|
||||
#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) {
|
||||
: 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() {
|
||||
}
|
||||
ShapeManager::~ShapeManager() = default;
|
||||
|
||||
void ShapeManager::initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr,
|
||||
StateManager* state_mgr, int screen_width, int screen_height) {
|
||||
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;
|
||||
@@ -65,17 +64,17 @@ void ShapeManager::toggleShapeMode(bool force_gravity_on_exit) {
|
||||
activateShapeInternal(last_shape_type_);
|
||||
|
||||
// Si estamos en modo LOGO y la figura es PNG_SHAPE, restaurar configuración LOGO
|
||||
if (state_mgr_ && state_mgr_->getCurrentMode() == AppMode::LOGO && last_shape_type_ == ShapeType::PNG_SHAPE) {
|
||||
if ((state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::LOGO && last_shape_type_ == ShapeType::PNG_SHAPE) {
|
||||
if (active_shape_) {
|
||||
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
|
||||
if (png_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_ && state_mgr_->getCurrentMode() == AppMode::LOGO) {
|
||||
if ((state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::LOGO) {
|
||||
shape_convergence_ = 0.0f;
|
||||
}
|
||||
} else {
|
||||
@@ -92,8 +91,8 @@ void ShapeManager::toggleShapeMode(bool force_gravity_on_exit) {
|
||||
}
|
||||
|
||||
// Mostrar notificación (solo si NO estamos en modo demo o logo)
|
||||
if (state_mgr_ && ui_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||
ui_mgr_->showNotification("Modo Física");
|
||||
if ((state_mgr_ != nullptr) && (ui_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||
ui_mgr_->showNotification("Modo física");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,8 +111,8 @@ void ShapeManager::handleShapeScaleChange(bool increase) {
|
||||
clampShapeScale();
|
||||
|
||||
// Mostrar notificación si está en modo SANDBOX
|
||||
if (ui_mgr_ && state_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||
std::string notification = "Escala " + std::to_string(static_cast<int>(shape_scale_factor_ * 100.0f + 0.5f)) + "%";
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -124,7 +123,7 @@ void ShapeManager::resetShapeScale() {
|
||||
shape_scale_factor_ = SHAPE_SCALE_DEFAULT;
|
||||
|
||||
// Mostrar notificación si está en modo SANDBOX
|
||||
if (ui_mgr_ && state_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||
if ((ui_mgr_ != nullptr) && (state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||
ui_mgr_->showNotification("Escala 100%");
|
||||
}
|
||||
}
|
||||
@@ -135,14 +134,16 @@ void ShapeManager::toggleDepthZoom() {
|
||||
depth_zoom_enabled_ = !depth_zoom_enabled_;
|
||||
|
||||
// Mostrar notificación si está en modo SANDBOX
|
||||
if (ui_mgr_ && state_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||
ui_mgr_->showNotification(depth_zoom_enabled_ ? "Profundidad On" : "Profundidad Off");
|
||||
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;
|
||||
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_));
|
||||
@@ -160,7 +161,9 @@ void ShapeManager::update(float delta_time) {
|
||||
// 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, y_3d, z_3d;
|
||||
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
|
||||
@@ -178,9 +181,7 @@ void ShapeManager::update(float delta_time) {
|
||||
// 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);
|
||||
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)
|
||||
@@ -190,12 +191,12 @@ void ShapeManager::update(float delta_time) {
|
||||
|
||||
// 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;
|
||||
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_ && state_mgr_->getCurrentMode() == AppMode::LOGO && current_mode_ == SimulationMode::SHAPE) {
|
||||
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)
|
||||
|
||||
@@ -214,7 +215,9 @@ void ShapeManager::update(float delta_time) {
|
||||
}
|
||||
|
||||
void ShapeManager::generateShape() {
|
||||
if (!active_shape_) return;
|
||||
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_));
|
||||
@@ -276,8 +279,10 @@ void ShapeManager::activateShapeInternal(ShapeType type) {
|
||||
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_ && ui_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
|
||||
std::string notification = std::string("Modo ") + active_shape_->getName();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +46,7 @@ class ShapeManager {
|
||||
* @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);
|
||||
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
|
||||
@@ -147,10 +146,10 @@ class ShapeManager {
|
||||
|
||||
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
|
||||
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_;
|
||||
|
||||
@@ -1,39 +1,37 @@
|
||||
#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_mgr/shape_manager.hpp" // for ShapeManager
|
||||
#include "shapes/png_shape.hpp" // for PNGShape flip detection
|
||||
#include "theme_manager.hpp" // for ThemeManager
|
||||
#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) {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -53,8 +51,10 @@ void StateManager::setLogoPreviousState(int theme, size_t texture_index, float s
|
||||
// ACTUALIZACIÓN DE ESTADOS
|
||||
// ===========================================================================
|
||||
|
||||
void StateManager::update(float delta_time, float shape_convergence, Shape* active_shape) {
|
||||
if (current_app_mode_ == AppMode::SANDBOX) return;
|
||||
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;
|
||||
|
||||
@@ -63,14 +63,12 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
|
||||
if (current_app_mode_ == AppMode::LOGO) {
|
||||
if (logo_waiting_for_flip_) {
|
||||
// CAMINO B: Esperando a que ocurran flips
|
||||
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape);
|
||||
auto* png_shape = dynamic_cast<PNGShape*>(active_shape);
|
||||
|
||||
if (png_shape) {
|
||||
if (png_shape != nullptr) {
|
||||
int current_flip_count = png_shape->getFlipCount();
|
||||
|
||||
if (current_flip_count > logo_current_flip_count_) {
|
||||
logo_current_flip_count_ = current_flip_count;
|
||||
}
|
||||
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()) {
|
||||
@@ -93,7 +91,9 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
|
||||
should_trigger = demo_timer_ >= demo_next_action_time_;
|
||||
}
|
||||
|
||||
if (!should_trigger) return;
|
||||
if (!should_trigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (current_app_mode_ == AppMode::LOGO) {
|
||||
// LOGO MODE: Sistema de acciones variadas con gravedad dinámica
|
||||
@@ -104,7 +104,7 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
|
||||
if (logo_waiting_for_flip_) {
|
||||
// Ya estábamos esperando flips → hacer el cambio SHAPE → PHYSICS
|
||||
if (action < 50) {
|
||||
engine_->exitShapeMode(true); // Con gravedad ON
|
||||
engine_->exitShapeMode(true); // Con gravedad ON
|
||||
} else {
|
||||
engine_->exitShapeMode(false); // Con gravedad OFF
|
||||
}
|
||||
@@ -122,15 +122,15 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
|
||||
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;
|
||||
|
||||
PNGShape* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
|
||||
if (png_shape) {
|
||||
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
|
||||
engine_->exitShapeMode(true); // SHAPE → PHYSICS con gravedad ON
|
||||
} else {
|
||||
engine_->exitShapeMode(false); // SHAPE → PHYSICS con gravedad OFF
|
||||
}
|
||||
@@ -158,7 +158,7 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
|
||||
scene_mgr_->forceBallsGravityOff();
|
||||
} else {
|
||||
// 16%: Cambiar dirección de gravedad
|
||||
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||
auto new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||
scene_mgr_->changeGravityDirection(new_direction);
|
||||
scene_mgr_->forceBallsGravityOn();
|
||||
}
|
||||
@@ -186,7 +186,9 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
|
||||
}
|
||||
|
||||
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_ == new_mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (current_app_mode_ == AppMode::LOGO && new_mode != AppMode::LOGO) {
|
||||
previous_app_mode_ = new_mode;
|
||||
@@ -201,7 +203,8 @@ void StateManager::setState(AppMode new_mode, int current_screen_width, int curr
|
||||
demo_timer_ = 0.0f;
|
||||
|
||||
if (new_mode == AppMode::DEMO || new_mode == AppMode::DEMO_LITE || new_mode == AppMode::LOGO) {
|
||||
float min_interval, max_interval;
|
||||
float min_interval;
|
||||
float max_interval;
|
||||
|
||||
if (new_mode == AppMode::LOGO) {
|
||||
float resolution_scale = current_screen_height / 720.0f;
|
||||
@@ -250,8 +253,10 @@ void StateManager::toggleLogoMode(int current_screen_width, int current_screen_h
|
||||
// ACCIONES DE DEMO
|
||||
// ===========================================================================
|
||||
|
||||
void StateManager::performDemoAction(bool is_lite) {
|
||||
if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return;
|
||||
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)
|
||||
@@ -278,18 +283,18 @@ void StateManager::performDemoAction(bool is_lite) {
|
||||
// ACCIONES NORMALES DE DEMO/DEMO_LITE
|
||||
// ============================================
|
||||
|
||||
int TOTAL_WEIGHT;
|
||||
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;
|
||||
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) {
|
||||
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||
auto new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||
scene_mgr_->changeGravityDirection(new_direction);
|
||||
return;
|
||||
}
|
||||
@@ -304,8 +309,8 @@ void StateManager::performDemoAction(bool is_lite) {
|
||||
// Activar figura 3D (25%) - PNG_SHAPE excluido
|
||||
accumulated_weight += DEMO_LITE_WEIGHT_SHAPE;
|
||||
if (random_value < accumulated_weight) {
|
||||
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
||||
engine_->enterShapeMode(shapes[rand() % 8]);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -324,13 +329,13 @@ void StateManager::performDemoAction(bool is_lite) {
|
||||
}
|
||||
|
||||
} 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;
|
||||
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) {
|
||||
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||
auto new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||
scene_mgr_->changeGravityDirection(new_direction);
|
||||
return;
|
||||
}
|
||||
@@ -345,8 +350,8 @@ void StateManager::performDemoAction(bool is_lite) {
|
||||
// Activar figura 3D (20%) - PNG_SHAPE excluido
|
||||
accumulated_weight += DEMO_WEIGHT_SHAPE;
|
||||
if (random_value < accumulated_weight) {
|
||||
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
||||
engine_->enterShapeMode(shapes[rand() % 8]);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -378,10 +383,12 @@ void StateManager::performDemoAction(bool is_lite) {
|
||||
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)
|
||||
for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) {
|
||||
candidates.push_back(i);
|
||||
if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable())
|
||||
}
|
||||
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);
|
||||
@@ -439,15 +446,15 @@ void StateManager::performDemoAction(bool is_lite) {
|
||||
// RANDOMIZACIÓN AL INICIAR DEMO
|
||||
// ===========================================================================
|
||||
|
||||
void StateManager::randomizeOnDemoStart(bool is_lite) {
|
||||
if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return;
|
||||
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) {
|
||||
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX,
|
||||
ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER,
|
||||
ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
||||
engine_->enterShapeMode(shapes[rand() % 8]);
|
||||
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) {
|
||||
@@ -457,11 +464,11 @@ void StateManager::randomizeOnDemoStart(bool is_lite) {
|
||||
engine_->exitShapeMode(false);
|
||||
}
|
||||
} else {
|
||||
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
||||
engine_->enterShapeMode(shapes[rand() % 8]);
|
||||
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]);
|
||||
}
|
||||
|
||||
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||
auto new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||
scene_mgr_->changeGravityDirection(new_direction);
|
||||
if (rand() % 2 == 0) {
|
||||
toggleGravityOnOff();
|
||||
@@ -476,14 +483,14 @@ void StateManager::randomizeOnDemoStart(bool is_lite) {
|
||||
engine_->exitShapeMode(false);
|
||||
}
|
||||
} else {
|
||||
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
||||
ShapeType selected_shape = shapes[rand() % 8];
|
||||
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);
|
||||
shape_mgr_->setShapeScaleFactor(0.5f + ((rand() % 1500) / 1000.0f));
|
||||
|
||||
engine_->enterShapeMode(selected_shape);
|
||||
}
|
||||
@@ -491,10 +498,12 @@ void StateManager::randomizeOnDemoStart(bool is_lite) {
|
||||
// 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)
|
||||
for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) {
|
||||
candidates.push_back(i);
|
||||
if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable())
|
||||
}
|
||||
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);
|
||||
@@ -513,7 +522,7 @@ void StateManager::randomizeOnDemoStart(bool is_lite) {
|
||||
}
|
||||
|
||||
// 5. Gravedad
|
||||
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||
auto new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||
scene_mgr_->changeGravityDirection(new_direction);
|
||||
if (rand() % 3 == 0) {
|
||||
toggleGravityOnOff();
|
||||
@@ -526,10 +535,12 @@ void StateManager::randomizeOnDemoStart(bool is_lite) {
|
||||
// ===========================================================================
|
||||
|
||||
void StateManager::toggleGravityOnOff() {
|
||||
if (!scene_mgr_) return;
|
||||
if (scene_mgr_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool gravity_enabled = scene_mgr_->hasBalls() &&
|
||||
(scene_mgr_->getFirstBall()->getGravityForce() > 0.0f);
|
||||
(scene_mgr_->getFirstBall()->getGravityForce() > 0.0f);
|
||||
|
||||
if (gravity_enabled) {
|
||||
scene_mgr_->forceBallsGravityOff();
|
||||
@@ -543,7 +554,9 @@ void StateManager::toggleGravityOnOff() {
|
||||
// ===========================================================================
|
||||
|
||||
void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int current_screen_height, size_t ball_count) {
|
||||
if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return;
|
||||
if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
logo_entered_manually_ = !from_demo;
|
||||
|
||||
@@ -585,8 +598,8 @@ void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int c
|
||||
}
|
||||
|
||||
// Cambiar a tema aleatorio entre: MONOCHROME, LAVENDER, CRIMSON, ESMERALDA
|
||||
int logo_themes[] = {5, 6, 7, 8};
|
||||
theme_mgr_->switchToTheme(logo_themes[rand() % 4]);
|
||||
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);
|
||||
@@ -595,8 +608,8 @@ void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int c
|
||||
engine_->enterShapeMode(ShapeType::PNG_SHAPE);
|
||||
|
||||
// Configurar PNG_SHAPE en modo LOGO
|
||||
PNGShape* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
|
||||
if (png_shape) {
|
||||
auto* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
|
||||
if (png_shape != nullptr) {
|
||||
png_shape->setLogoMode(true);
|
||||
png_shape->resetFlipCount();
|
||||
}
|
||||
@@ -607,8 +620,12 @@ void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int c
|
||||
// ===========================================================================
|
||||
|
||||
void StateManager::exitLogoMode(bool return_to_demo) {
|
||||
if (current_app_mode_ != AppMode::LOGO) return;
|
||||
if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return;
|
||||
if (current_app_mode_ != AppMode::LOGO) {
|
||||
return;
|
||||
}
|
||||
if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
logo_entered_manually_ = false;
|
||||
|
||||
@@ -624,17 +641,15 @@ void StateManager::exitLogoMode(bool return_to_demo) {
|
||||
}
|
||||
|
||||
// Desactivar modo LOGO en PNG_SHAPE
|
||||
PNGShape* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
|
||||
if (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) {
|
||||
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX,
|
||||
ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER,
|
||||
ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
||||
engine_->enterShapeMode(shapes[rand() % 8]);
|
||||
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) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_stdinc.h> // for Uint64
|
||||
#include <cstddef> // for size_t
|
||||
|
||||
#include <cstddef> // for size_t
|
||||
|
||||
#include "defines.hpp" // for AppMode, ShapeType, GravityDirection
|
||||
|
||||
@@ -37,7 +38,7 @@ class StateManager {
|
||||
/**
|
||||
* @brief Destructor
|
||||
*/
|
||||
~StateManager();
|
||||
~StateManager() = default;
|
||||
|
||||
/**
|
||||
* @brief Inicializa el StateManager con referencias a los subsistemas necesarios
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
#include "textrenderer.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "resource_manager.hpp"
|
||||
|
||||
TextRenderer::TextRenderer() : renderer_(nullptr), font_(nullptr), font_size_(0), use_antialiasing_(true), font_data_buffer_(nullptr) {
|
||||
TextRenderer::TextRenderer()
|
||||
: renderer_(nullptr),
|
||||
font_(nullptr),
|
||||
font_size_(0),
|
||||
use_antialiasing_(true),
|
||||
font_data_buffer_(nullptr) {
|
||||
}
|
||||
|
||||
TextRenderer::~TextRenderer() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
bool TextRenderer::init(SDL_Renderer* renderer, const char* font_path, int font_size, bool use_antialiasing) {
|
||||
auto TextRenderer::init(SDL_Renderer* renderer, const char* font_path, int font_size, bool use_antialiasing) -> bool {
|
||||
renderer_ = renderer;
|
||||
font_size_ = font_size;
|
||||
use_antialiasing_ = use_antialiasing;
|
||||
font_path_ = font_path; // Guardar ruta para reinitialize()
|
||||
|
||||
// Inicializar SDL_ttf si no está inicializado
|
||||
if (!TTF_WasInit()) {
|
||||
if (TTF_WasInit() == 0) {
|
||||
if (!TTF_Init()) {
|
||||
SDL_Log("Error al inicializar SDL_ttf: %s", SDL_GetError());
|
||||
return false;
|
||||
@@ -25,30 +34,33 @@ bool TextRenderer::init(SDL_Renderer* renderer, const char* font_path, int font_
|
||||
}
|
||||
|
||||
// Intentar cargar la fuente desde ResourceManager (pack o disco)
|
||||
unsigned char* fontData = nullptr;
|
||||
size_t fontDataSize = 0;
|
||||
unsigned char* font_data = nullptr;
|
||||
size_t font_data_size = 0;
|
||||
|
||||
if (ResourceManager::loadResource(font_path, fontData, fontDataSize)) {
|
||||
if (ResourceManager::loadResource(font_path, font_data, font_data_size)) {
|
||||
// Crear SDL_IOStream desde memoria
|
||||
SDL_IOStream* fontIO = SDL_IOFromConstMem(fontData, static_cast<size_t>(fontDataSize));
|
||||
if (fontIO != nullptr) {
|
||||
SDL_IOStream* font_io = SDL_IOFromConstMem(font_data, font_data_size);
|
||||
if (font_io != nullptr) {
|
||||
// Cargar fuente desde IOStream
|
||||
font_ = TTF_OpenFontIO(fontIO, true, font_size); // true = cerrar stream automáticamente
|
||||
font_ = TTF_OpenFontIO(font_io, true, font_size); // true = cerrar stream automáticamente
|
||||
|
||||
if (font_ == nullptr) {
|
||||
SDL_Log("Error al cargar fuente desde memoria '%s': %s", font_path, SDL_GetError());
|
||||
delete[] fontData; // Liberar solo si falla la carga
|
||||
delete[] font_data; // Liberar solo si falla la carga
|
||||
return false;
|
||||
}
|
||||
|
||||
// CRÍTICO: NO eliminar fontData aquí - SDL_ttf necesita estos datos en memoria
|
||||
// mientras la fuente esté abierta. Se liberará en cleanup()
|
||||
font_data_buffer_ = fontData;
|
||||
SDL_Log("Fuente cargada desde ResourceManager: %s (%lu bytes)", font_path, (unsigned long)fontDataSize);
|
||||
font_data_buffer_ = font_data;
|
||||
{
|
||||
std::string fn = std::string(font_path);
|
||||
fn = fn.substr(fn.find_last_of("\\/") + 1);
|
||||
std::cout << "[Fuente] " << fn << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
delete[] fontData;
|
||||
}
|
||||
delete[] font_data;
|
||||
}
|
||||
|
||||
// Fallback final: intentar cargar directamente desde disco (por si falla ResourceManager)
|
||||
@@ -61,7 +73,7 @@ bool TextRenderer::init(SDL_Renderer* renderer, const char* font_path, int font_
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextRenderer::reinitialize(int new_font_size) {
|
||||
auto TextRenderer::reinitialize(int new_font_size) -> bool {
|
||||
// Verificar que tenemos todo lo necesario
|
||||
if (renderer_ == nullptr || font_path_.empty()) {
|
||||
SDL_Log("Error: TextRenderer no inicializado correctamente para reinitialize()");
|
||||
@@ -84,36 +96,42 @@ bool TextRenderer::reinitialize(int new_font_size) {
|
||||
}
|
||||
|
||||
// Intentar cargar la fuente desde ResourceManager con el nuevo tamaño
|
||||
unsigned char* fontData = nullptr;
|
||||
size_t fontDataSize = 0;
|
||||
unsigned char* font_data = nullptr;
|
||||
size_t font_data_size = 0;
|
||||
|
||||
if (ResourceManager::loadResource(font_path_, fontData, fontDataSize)) {
|
||||
SDL_IOStream* fontIO = SDL_IOFromConstMem(fontData, static_cast<size_t>(fontDataSize));
|
||||
if (fontIO != nullptr) {
|
||||
font_ = TTF_OpenFontIO(fontIO, true, new_font_size);
|
||||
if (ResourceManager::loadResource(font_path_, font_data, font_data_size)) {
|
||||
SDL_IOStream* font_io = SDL_IOFromConstMem(font_data, font_data_size);
|
||||
if (font_io != nullptr) {
|
||||
font_ = TTF_OpenFontIO(font_io, true, new_font_size);
|
||||
|
||||
if (font_ == nullptr) {
|
||||
SDL_Log("Error al recargar fuente '%s' con tamaño %d: %s",
|
||||
font_path_.c_str(), new_font_size, SDL_GetError());
|
||||
delete[] fontData; // Liberar solo si falla
|
||||
font_path_.c_str(),
|
||||
new_font_size,
|
||||
SDL_GetError());
|
||||
delete[] font_data; // Liberar solo si falla
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mantener buffer en memoria (NO eliminar)
|
||||
font_data_buffer_ = fontData;
|
||||
font_data_buffer_ = font_data;
|
||||
font_size_ = new_font_size;
|
||||
SDL_Log("Fuente recargada desde ResourceManager: %s (tamaño %d)", font_path_.c_str(), new_font_size);
|
||||
{
|
||||
std::string fn = font_path_.substr(font_path_.find_last_of("\\/") + 1);
|
||||
std::cout << "[Fuente] " << fn << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
delete[] fontData;
|
||||
}
|
||||
delete[] font_data;
|
||||
}
|
||||
|
||||
// Fallback: cargar directamente desde disco
|
||||
font_ = TTF_OpenFont(font_path_.c_str(), new_font_size);
|
||||
if (font_ == nullptr) {
|
||||
SDL_Log("Error al recargar fuente '%s' con tamaño %d: %s",
|
||||
font_path_.c_str(), new_font_size, SDL_GetError());
|
||||
font_path_.c_str(),
|
||||
new_font_size,
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -225,7 +243,8 @@ void TextRenderer::printPhysical(int logical_x, int logical_y, const char* text,
|
||||
dest_rect.h = static_cast<float>(text_surface->h);
|
||||
|
||||
// Deshabilitar temporalmente presentación lógica para renderizar en píxeles físicos
|
||||
int logical_w = 0, logical_h = 0;
|
||||
int logical_w = 0;
|
||||
int logical_h = 0;
|
||||
SDL_RendererLogicalPresentation presentation_mode;
|
||||
SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &presentation_mode);
|
||||
|
||||
@@ -293,7 +312,8 @@ void TextRenderer::printAbsolute(int physical_x, int physical_y, const char* tex
|
||||
dest_rect.h = static_cast<float>(text_surface->h);
|
||||
|
||||
// Deshabilitar temporalmente presentación lógica para renderizar en píxeles físicos
|
||||
int logical_w = 0, logical_h = 0;
|
||||
int logical_w = 0;
|
||||
int logical_h = 0;
|
||||
SDL_RendererLogicalPresentation presentation_mode;
|
||||
SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &presentation_mode);
|
||||
|
||||
@@ -324,7 +344,7 @@ void TextRenderer::printAbsoluteShadowed(int physical_x, int physical_y, const s
|
||||
printAbsoluteShadowed(physical_x, physical_y, text.c_str());
|
||||
}
|
||||
|
||||
int TextRenderer::getTextWidth(const char* text) {
|
||||
auto TextRenderer::getTextWidth(const char* text) -> int {
|
||||
if (!isInitialized() || text == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
@@ -337,7 +357,7 @@ int TextRenderer::getTextWidth(const char* text) {
|
||||
return width;
|
||||
}
|
||||
|
||||
int TextRenderer::getTextWidthPhysical(const char* text) {
|
||||
auto TextRenderer::getTextWidthPhysical(const char* text) -> int {
|
||||
// Retorna el ancho REAL en píxeles físicos (sin escalado lógico)
|
||||
// Idéntico a getTextWidth() pero semánticamente diferente:
|
||||
// - Este método se usa cuando se necesita el ancho REAL de la fuente
|
||||
@@ -354,10 +374,18 @@ int TextRenderer::getTextWidthPhysical(const char* text) {
|
||||
return width; // Ancho real de la textura generada por TTF
|
||||
}
|
||||
|
||||
int TextRenderer::getTextHeight() {
|
||||
auto TextRenderer::getTextHeight() -> int {
|
||||
if (!isInitialized()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return TTF_GetFontHeight(font_);
|
||||
}
|
||||
|
||||
auto TextRenderer::getGlyphHeight() -> int {
|
||||
if (!isInitialized()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return TTF_GetFontAscent(font_) - TTF_GetFontDescent(font_);
|
||||
}
|
||||
|
||||
@@ -2,57 +2,61 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
class TextRenderer {
|
||||
public:
|
||||
TextRenderer();
|
||||
~TextRenderer();
|
||||
public:
|
||||
TextRenderer();
|
||||
~TextRenderer();
|
||||
|
||||
// Inicializa el renderizador de texto con una fuente
|
||||
bool init(SDL_Renderer* renderer, const char* font_path, int font_size, bool use_antialiasing = true);
|
||||
// Inicializa el renderizador de texto con una fuente
|
||||
bool init(SDL_Renderer* renderer, const char* font_path, int font_size, bool use_antialiasing = true);
|
||||
|
||||
// Reinicializa el renderizador con un nuevo tamaño de fuente
|
||||
bool reinitialize(int new_font_size);
|
||||
// Reinicializa el renderizador con un nuevo tamaño de fuente
|
||||
bool reinitialize(int new_font_size);
|
||||
|
||||
// Libera recursos
|
||||
void cleanup();
|
||||
// Libera recursos
|
||||
void cleanup();
|
||||
|
||||
// Renderiza texto en la posición especificada con color RGB
|
||||
void print(int x, int y, const char* text, uint8_t r, uint8_t g, uint8_t b);
|
||||
void print(int x, int y, const std::string& text, uint8_t r, uint8_t g, uint8_t b);
|
||||
// Renderiza texto en la posición especificada con color RGB
|
||||
void print(int x, int y, const char* text, uint8_t r, uint8_t g, uint8_t b);
|
||||
void print(int x, int y, const std::string& text, uint8_t r, uint8_t g, uint8_t b);
|
||||
|
||||
// Renderiza texto en coordenadas lógicas, pero convierte a físicas para tamaño absoluto
|
||||
void printPhysical(int logical_x, int logical_y, const char* text, uint8_t r, uint8_t g, uint8_t b, float scale_x, float scale_y);
|
||||
void printPhysical(int logical_x, int logical_y, const std::string& text, uint8_t r, uint8_t g, uint8_t b, float scale_x, float scale_y);
|
||||
// Renderiza texto en coordenadas lógicas, pero convierte a físicas para tamaño absoluto
|
||||
void printPhysical(int logical_x, int logical_y, const char* text, uint8_t r, uint8_t g, uint8_t b, float scale_x, float scale_y);
|
||||
void printPhysical(int logical_x, int logical_y, const std::string& text, uint8_t r, uint8_t g, uint8_t b, float scale_x, float scale_y);
|
||||
|
||||
// Renderiza texto en coordenadas físicas absolutas (tamaño fijo independiente de resolución)
|
||||
// NOTA: Este método usa el tamaño de fuente tal cual fue cargado, sin escalado
|
||||
void printAbsolute(int physical_x, int physical_y, const char* text, SDL_Color color);
|
||||
void printAbsolute(int physical_x, int physical_y, const std::string& text, SDL_Color color);
|
||||
// Renderiza texto en coordenadas físicas absolutas (tamaño fijo independiente de resolución)
|
||||
// NOTA: Este método usa el tamaño de fuente tal cual fue cargado, sin escalado
|
||||
void printAbsolute(int physical_x, int physical_y, const char* text, SDL_Color color);
|
||||
void printAbsolute(int physical_x, int physical_y, const std::string& text, SDL_Color color);
|
||||
|
||||
// Renderiza texto con sombra negra (+1px offset) para máxima legibilidad sobre cualquier fondo
|
||||
void printAbsoluteShadowed(int physical_x, int physical_y, const char* text);
|
||||
void printAbsoluteShadowed(int physical_x, int physical_y, const std::string& text);
|
||||
// Renderiza texto con sombra negra (+1px offset) para máxima legibilidad sobre cualquier fondo
|
||||
void printAbsoluteShadowed(int physical_x, int physical_y, const char* text);
|
||||
void printAbsoluteShadowed(int physical_x, int physical_y, const std::string& text);
|
||||
|
||||
// Obtiene el ancho de un texto renderizado (en píxeles lógicos para compatibilidad)
|
||||
int getTextWidth(const char* text);
|
||||
// Obtiene el ancho de un texto renderizado (en píxeles lógicos para compatibilidad)
|
||||
int getTextWidth(const char* text);
|
||||
|
||||
// Obtiene el ancho de un texto en píxeles FÍSICOS reales (sin escalado)
|
||||
// Útil para notificaciones y elementos UI de tamaño fijo
|
||||
int getTextWidthPhysical(const char* text);
|
||||
// Obtiene el ancho de un texto en píxeles FÍSICOS reales (sin escalado)
|
||||
// Útil para notificaciones y elementos UI de tamaño fijo
|
||||
int getTextWidthPhysical(const char* text);
|
||||
|
||||
// Obtiene la altura de la fuente
|
||||
int getTextHeight();
|
||||
// Obtiene la altura de la fuente (incluye line_gap)
|
||||
int getTextHeight();
|
||||
|
||||
// Verifica si está inicializado correctamente
|
||||
bool isInitialized() const { return font_ != nullptr && renderer_ != nullptr; }
|
||||
// Obtiene la altura real del glifo (ascender + |descendente|, sin line_gap)
|
||||
int getGlyphHeight();
|
||||
|
||||
private:
|
||||
SDL_Renderer* renderer_;
|
||||
TTF_Font* font_;
|
||||
int font_size_;
|
||||
bool use_antialiasing_;
|
||||
std::string font_path_; // Almacenar ruta para reinitialize()
|
||||
unsigned char* font_data_buffer_; // Buffer de datos de fuente (mantener en memoria mientras esté abierta)
|
||||
// Verifica si está inicializado correctamente
|
||||
bool isInitialized() const { return font_ != nullptr && renderer_ != nullptr; }
|
||||
|
||||
private:
|
||||
SDL_Renderer* renderer_;
|
||||
TTF_Font* font_;
|
||||
int font_size_;
|
||||
bool use_antialiasing_;
|
||||
std::string font_path_; // Almacenar ruta para reinitialize()
|
||||
unsigned char* font_data_buffer_; // Buffer de datos de fuente (mantener en memoria mientras esté abierta)
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,10 +3,10 @@
|
||||
#include <memory> // for unique_ptr
|
||||
#include <vector> // for vector
|
||||
|
||||
#include "ball.hpp" // for Ball class
|
||||
#include "defines.hpp" // for Color, ColorTheme
|
||||
#include "themes/theme.hpp" // for Theme interface
|
||||
#include "themes/theme_snapshot.hpp" // for ThemeSnapshot
|
||||
#include "ball.hpp" // for Ball class
|
||||
#include "defines.hpp" // for Color, ColorTheme
|
||||
#include "themes/theme.hpp" // for Theme interface
|
||||
#include "themes/theme_snapshot.hpp" // for ThemeSnapshot
|
||||
|
||||
/**
|
||||
* ThemeManager: Gestiona el sistema de temas visuales (unificado, estáticos y dinámicos)
|
||||
@@ -42,7 +42,8 @@ class ThemeManager {
|
||||
~ThemeManager() = default;
|
||||
|
||||
// Inicialización
|
||||
void initialize(); // Inicializa 15 temas unificados (9 estáticos + 6 dinámicos)
|
||||
void initialize(); // Inicializa 15 temas unificados (9 estáticos + 6 dinámicos)
|
||||
void setMaxBallCount(int n) { max_ball_count_ = n; } // Máximo real (escenario 8 o custom si mayor)
|
||||
|
||||
// Interfaz unificada (PHASE 2 + PHASE 3)
|
||||
void switchToTheme(int theme_index); // Cambia a tema 0-14 con transición LERP suave (PHASE 3)
|
||||
@@ -52,9 +53,8 @@ class ThemeManager {
|
||||
void pauseDynamic(); // Toggle pausa de animación (Shift+D, solo dinámicos)
|
||||
|
||||
// Queries de colores (usado en rendering)
|
||||
Color getInterpolatedColor(size_t ball_index) const; // Obtiene color interpolado para pelota
|
||||
void getBackgroundColors(float& top_r, float& top_g, float& top_b,
|
||||
float& bottom_r, float& bottom_g, float& bottom_b) const; // Obtiene colores de fondo degradado
|
||||
Color getInterpolatedColor(size_t ball_index) const; // Obtiene color interpolado para pelota
|
||||
void getBackgroundColors(float& top_r, float& top_g, float& top_b, float& bottom_r, float& bottom_g, float& bottom_b) const; // Obtiene colores de fondo degradado
|
||||
|
||||
// Queries de estado (para debug display y lógica)
|
||||
int getCurrentThemeIndex() const { return current_theme_index_; }
|
||||
@@ -88,17 +88,20 @@ class ThemeManager {
|
||||
// ========================================
|
||||
|
||||
// Estado de transición
|
||||
bool transitioning_ = false; // ¿Hay transición LERP activa?
|
||||
float transition_progress_ = 0.0f; // Progreso 0.0-1.0 (0.0 = origen, 1.0 = destino)
|
||||
bool transitioning_ = false; // ¿Hay transición LERP activa?
|
||||
float transition_progress_ = 0.0f; // Progreso 0.0-1.0 (0.0 = origen, 1.0 = destino)
|
||||
float transition_duration_ = THEME_TRANSITION_DURATION; // Duración en segundos (configurable en defines.h)
|
||||
|
||||
// Índices de temas involucrados en transición
|
||||
int source_theme_index_ = 0; // Tema origen (del que venimos)
|
||||
int target_theme_index_ = 0; // Tema destino (al que vamos)
|
||||
int source_theme_index_ = 0; // Tema origen (del que venimos)
|
||||
int target_theme_index_ = 0; // Tema destino (al que vamos)
|
||||
|
||||
// Snapshot del tema origen (capturado al iniciar transición)
|
||||
std::unique_ptr<ThemeSnapshot> source_snapshot_; // nullptr si no hay transición
|
||||
|
||||
// Máximo de bolas posible en esta sesión (max(SCENE_BALLS_8, custom_balls))
|
||||
int max_ball_count_ = SCENE_BALLS_8;
|
||||
|
||||
// ========================================
|
||||
// MÉTODOS PRIVADOS
|
||||
// ========================================
|
||||
@@ -108,6 +111,6 @@ class ThemeManager {
|
||||
void initializeDynamicThemes(); // Crea 6 temas dinámicos (índices 9-14)
|
||||
|
||||
// Sistema de transición LERP (PHASE 3)
|
||||
std::unique_ptr<ThemeSnapshot> captureCurrentSnapshot() const; // Captura snapshot del tema actual
|
||||
std::unique_ptr<ThemeSnapshot> captureCurrentSnapshot() const; // Captura snapshot del tema actual
|
||||
float lerp(float a, float b, float t) const { return a + (b - a) * t; } // Interpolación lineal
|
||||
};
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
#include "dynamic_theme.hpp"
|
||||
|
||||
#include <algorithm> // for std::min
|
||||
|
||||
DynamicTheme::DynamicTheme(const char* name_en, const char* name_es,
|
||||
int text_r, int text_g, int text_b,
|
||||
std::vector<DynamicThemeKeyframe> keyframes,
|
||||
bool loop)
|
||||
DynamicTheme::DynamicTheme(const char* name_en, const char* name_es, int text_r, int text_g, int text_b, std::vector<DynamicThemeKeyframe> keyframes, bool loop)
|
||||
: name_en_(name_en),
|
||||
name_es_(name_es),
|
||||
text_r_(text_r), text_g_(text_g), text_b_(text_b),
|
||||
text_r_(text_r),
|
||||
text_g_(text_g),
|
||||
text_b_(text_b),
|
||||
keyframes_(std::move(keyframes)),
|
||||
loop_(loop),
|
||||
current_keyframe_index_(0),
|
||||
target_keyframe_index_(1),
|
||||
transition_progress_(0.0f),
|
||||
paused_(false) {
|
||||
loop_(loop) {
|
||||
// Validación: mínimo 2 keyframes
|
||||
if (keyframes_.size() < 2) {
|
||||
// Fallback: duplicar primer keyframe si solo hay 1
|
||||
@@ -29,7 +25,9 @@ DynamicTheme::DynamicTheme(const char* name_en, const char* name_es,
|
||||
}
|
||||
|
||||
void DynamicTheme::update(float delta_time) {
|
||||
if (paused_) return; // No actualizar si está pausado
|
||||
if (paused_) {
|
||||
return; // No actualizar si está pausado
|
||||
}
|
||||
|
||||
// Obtener duración del keyframe objetivo
|
||||
float duration = keyframes_[target_keyframe_index_].duration;
|
||||
@@ -74,14 +72,14 @@ void DynamicTheme::advanceToNextKeyframe() {
|
||||
transition_progress_ = 0.0f;
|
||||
}
|
||||
|
||||
Color DynamicTheme::getBallColor(size_t ball_index, float progress) const {
|
||||
auto DynamicTheme::getBallColor(size_t ball_index, float progress) const -> Color {
|
||||
// Obtener keyframes actual y objetivo
|
||||
const auto& current_kf = keyframes_[current_keyframe_index_];
|
||||
const auto& target_kf = keyframes_[target_keyframe_index_];
|
||||
|
||||
// Si paletas vacías, retornar blanco
|
||||
if (current_kf.ball_colors.empty() || target_kf.ball_colors.empty()) {
|
||||
return {255, 255, 255};
|
||||
return {.r = 255, .g = 255, .b = 255};
|
||||
}
|
||||
|
||||
// Obtener colores de ambos keyframes (con wrap)
|
||||
@@ -95,15 +93,18 @@ Color DynamicTheme::getBallColor(size_t ball_index, float progress) const {
|
||||
// (progress parámetro será usado en PHASE 3 para LERP externo)
|
||||
float t = transition_progress_;
|
||||
return {
|
||||
static_cast<int>(lerp(c1.r, c2.r, t)),
|
||||
static_cast<int>(lerp(c1.g, c2.g, t)),
|
||||
static_cast<int>(lerp(c1.b, c2.b, t))
|
||||
};
|
||||
.r = static_cast<int>(lerp(c1.r, c2.r, t)),
|
||||
.g = static_cast<int>(lerp(c1.g, c2.g, t)),
|
||||
.b = static_cast<int>(lerp(c1.b, c2.b, t))};
|
||||
}
|
||||
|
||||
void DynamicTheme::getBackgroundColors(float progress,
|
||||
float& tr, float& tg, float& tb,
|
||||
float& br, float& bg, float& bb) const {
|
||||
float& tr,
|
||||
float& tg,
|
||||
float& tb,
|
||||
float& br,
|
||||
float& bg,
|
||||
float& bb) const {
|
||||
// Obtener keyframes actual y objetivo
|
||||
const auto& current_kf = keyframes_[current_keyframe_index_];
|
||||
const auto& target_kf = keyframes_[target_keyframe_index_];
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "theme.hpp"
|
||||
#include <string>
|
||||
|
||||
#include "theme.hpp"
|
||||
|
||||
// Forward declaration (estructura definida en defines.h)
|
||||
struct DynamicThemeKeyframe;
|
||||
|
||||
@@ -30,10 +31,7 @@ class DynamicTheme : public Theme {
|
||||
* @param keyframes: Vector de keyframes (mínimo 2)
|
||||
* @param loop: ¿Volver al inicio al terminar? (siempre true en esta app)
|
||||
*/
|
||||
DynamicTheme(const char* name_en, const char* name_es,
|
||||
int text_r, int text_g, int text_b,
|
||||
std::vector<DynamicThemeKeyframe> keyframes,
|
||||
bool loop = true);
|
||||
DynamicTheme(const char* name_en, const char* name_es, int text_r, int text_g, int text_b, std::vector<DynamicThemeKeyframe> keyframes, bool loop = true);
|
||||
|
||||
~DynamicTheme() override = default;
|
||||
|
||||
@@ -56,8 +54,12 @@ class DynamicTheme : public Theme {
|
||||
|
||||
Color getBallColor(size_t ball_index, float progress) const override;
|
||||
void getBackgroundColors(float progress,
|
||||
float& tr, float& tg, float& tb,
|
||||
float& br, float& bg, float& bb) const override;
|
||||
float& tr,
|
||||
float& tg,
|
||||
float& tb,
|
||||
float& br,
|
||||
float& bg,
|
||||
float& bb) const override;
|
||||
|
||||
// ========================================
|
||||
// ANIMACIÓN (soporte completo)
|
||||
@@ -90,10 +92,10 @@ class DynamicTheme : public Theme {
|
||||
// ESTADO DE ANIMACIÓN
|
||||
// ========================================
|
||||
|
||||
size_t current_keyframe_index_ = 0; // Keyframe actual
|
||||
size_t target_keyframe_index_ = 1; // Próximo keyframe
|
||||
float transition_progress_ = 0.0f; // Progreso 0.0-1.0 hacia target
|
||||
bool paused_ = false; // Pausa manual con Shift+D
|
||||
size_t current_keyframe_index_ = 0; // Keyframe actual
|
||||
size_t target_keyframe_index_ = 1; // Próximo keyframe
|
||||
float transition_progress_ = 0.0f; // Progreso 0.0-1.0 hacia target
|
||||
bool paused_ = false; // Pausa manual con Shift+D
|
||||
|
||||
// ========================================
|
||||
// UTILIDADES PRIVADAS
|
||||
|
||||
@@ -1,32 +1,39 @@
|
||||
#include "static_theme.hpp"
|
||||
|
||||
StaticTheme::StaticTheme(const char* name_en, const char* name_es,
|
||||
int text_r, int text_g, int text_b,
|
||||
int notif_bg_r, int notif_bg_g, int notif_bg_b,
|
||||
float bg_top_r, float bg_top_g, float bg_top_b,
|
||||
float bg_bottom_r, float bg_bottom_g, float bg_bottom_b,
|
||||
std::vector<Color> ball_colors)
|
||||
StaticTheme::StaticTheme(const char* name_en, const char* name_es, int text_r, int text_g, int text_b, int notif_bg_r, int notif_bg_g, int notif_bg_b, float bg_top_r, float bg_top_g, float bg_top_b, float bg_bottom_r, float bg_bottom_g, float bg_bottom_b, std::vector<Color> ball_colors)
|
||||
: name_en_(name_en),
|
||||
name_es_(name_es),
|
||||
text_r_(text_r), text_g_(text_g), text_b_(text_b),
|
||||
notif_bg_r_(notif_bg_r), notif_bg_g_(notif_bg_g), notif_bg_b_(notif_bg_b),
|
||||
bg_top_r_(bg_top_r), bg_top_g_(bg_top_g), bg_top_b_(bg_top_b),
|
||||
bg_bottom_r_(bg_bottom_r), bg_bottom_g_(bg_bottom_g), bg_bottom_b_(bg_bottom_b),
|
||||
text_r_(text_r),
|
||||
text_g_(text_g),
|
||||
text_b_(text_b),
|
||||
notif_bg_r_(notif_bg_r),
|
||||
notif_bg_g_(notif_bg_g),
|
||||
notif_bg_b_(notif_bg_b),
|
||||
bg_top_r_(bg_top_r),
|
||||
bg_top_g_(bg_top_g),
|
||||
bg_top_b_(bg_top_b),
|
||||
bg_bottom_r_(bg_bottom_r),
|
||||
bg_bottom_g_(bg_bottom_g),
|
||||
bg_bottom_b_(bg_bottom_b),
|
||||
ball_colors_(std::move(ball_colors)) {
|
||||
}
|
||||
|
||||
Color StaticTheme::getBallColor(size_t ball_index, float progress) const {
|
||||
auto StaticTheme::getBallColor(size_t ball_index, float progress) const -> Color {
|
||||
// Tema estático: siempre retorna color de paleta según índice
|
||||
// (progress se ignora aquí, pero será usado en PHASE 3 para LERP externo)
|
||||
if (ball_colors_.empty()) {
|
||||
return {255, 255, 255}; // Blanco por defecto si paleta vacía
|
||||
return {.r = 255, .g = 255, .b = 255}; // Blanco por defecto si paleta vacía
|
||||
}
|
||||
return ball_colors_[ball_index % ball_colors_.size()];
|
||||
}
|
||||
|
||||
void StaticTheme::getBackgroundColors(float progress,
|
||||
float& tr, float& tg, float& tb,
|
||||
float& br, float& bg, float& bb) const {
|
||||
float& tr,
|
||||
float& tg,
|
||||
float& tb,
|
||||
float& br,
|
||||
float& bg,
|
||||
float& bb) const {
|
||||
// Tema estático: siempre retorna colores de fondo fijos
|
||||
// (progress se ignora aquí, pero será usado en PHASE 3 para LERP externo)
|
||||
tr = bg_top_r_;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "theme.hpp"
|
||||
#include <string>
|
||||
|
||||
#include "theme.hpp"
|
||||
|
||||
/**
|
||||
* StaticTheme: Tema estático con 1 keyframe (sin animación)
|
||||
*
|
||||
@@ -28,12 +29,7 @@ class StaticTheme : public Theme {
|
||||
* @param bg_bottom_r, bg_bottom_g, bg_bottom_b: Color inferior de fondo
|
||||
* @param ball_colors: Paleta de colores para pelotas
|
||||
*/
|
||||
StaticTheme(const char* name_en, const char* name_es,
|
||||
int text_r, int text_g, int text_b,
|
||||
int notif_bg_r, int notif_bg_g, int notif_bg_b,
|
||||
float bg_top_r, float bg_top_g, float bg_top_b,
|
||||
float bg_bottom_r, float bg_bottom_g, float bg_bottom_b,
|
||||
std::vector<Color> ball_colors);
|
||||
StaticTheme(const char* name_en, const char* name_es, int text_r, int text_g, int text_b, int notif_bg_r, int notif_bg_g, int notif_bg_b, float bg_top_r, float bg_top_g, float bg_top_b, float bg_bottom_r, float bg_bottom_g, float bg_bottom_b, std::vector<Color> ball_colors);
|
||||
|
||||
~StaticTheme() override = default;
|
||||
|
||||
@@ -60,8 +56,12 @@ class StaticTheme : public Theme {
|
||||
|
||||
Color getBallColor(size_t ball_index, float progress) const override;
|
||||
void getBackgroundColors(float progress,
|
||||
float& tr, float& tg, float& tb,
|
||||
float& br, float& bg, float& bb) const override;
|
||||
float& tr,
|
||||
float& tg,
|
||||
float& tb,
|
||||
float& br,
|
||||
float& bg,
|
||||
float& bb) const override;
|
||||
|
||||
// ========================================
|
||||
// ANIMACIÓN (sin soporte - tema estático)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "defines.hpp" // for Color, ThemeKeyframe
|
||||
|
||||
/**
|
||||
@@ -47,8 +48,12 @@ class Theme {
|
||||
* @param br, bg, bb: Color inferior (out)
|
||||
*/
|
||||
virtual void getBackgroundColors(float progress,
|
||||
float& tr, float& tg, float& tb,
|
||||
float& br, float& bg, float& bb) const = 0;
|
||||
float& tr,
|
||||
float& tg,
|
||||
float& tb,
|
||||
float& br,
|
||||
float& bg,
|
||||
float& bb) const = 0;
|
||||
|
||||
// ========================================
|
||||
// ANIMACIÓN (solo temas dinámicos)
|
||||
@@ -58,7 +63,7 @@ class Theme {
|
||||
* Actualiza progreso de animación interna (solo dinámicos)
|
||||
* @param delta_time: Tiempo transcurrido desde último frame
|
||||
*/
|
||||
virtual void update(float delta_time) { }
|
||||
virtual void update(float delta_time) {}
|
||||
|
||||
/**
|
||||
* ¿Este tema necesita update() cada frame?
|
||||
@@ -75,7 +80,7 @@ class Theme {
|
||||
/**
|
||||
* Reinicia progreso de animación a 0.0 (usado al activar tema)
|
||||
*/
|
||||
virtual void resetProgress() { }
|
||||
virtual void resetProgress() {}
|
||||
|
||||
// ========================================
|
||||
// PAUSA (solo temas dinámicos)
|
||||
@@ -90,5 +95,5 @@ class Theme {
|
||||
/**
|
||||
* Toggle pausa de animación (solo dinámicos, tecla Shift+D)
|
||||
*/
|
||||
virtual void togglePause() { }
|
||||
virtual void togglePause() {}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "defines.hpp" // for Color
|
||||
|
||||
/**
|
||||
@@ -24,23 +25,23 @@
|
||||
* - Nombres del tema (para debug display durante transición)
|
||||
*/
|
||||
struct ThemeSnapshot {
|
||||
// Colores de fondo degradado
|
||||
float bg_top_r, bg_top_g, bg_top_b;
|
||||
float bg_bottom_r, bg_bottom_g, bg_bottom_b;
|
||||
// Colores de fondo degradado
|
||||
float bg_top_r, bg_top_g, bg_top_b;
|
||||
float bg_bottom_r, bg_bottom_g, bg_bottom_b;
|
||||
|
||||
// Color de texto UI
|
||||
int text_color_r, text_color_g, text_color_b;
|
||||
// Color de texto UI
|
||||
int text_color_r, text_color_g, text_color_b;
|
||||
|
||||
// Color de fondo de notificaciones
|
||||
int notif_bg_r, notif_bg_g, notif_bg_b;
|
||||
// Color de fondo de notificaciones
|
||||
int notif_bg_r, notif_bg_g, notif_bg_b;
|
||||
|
||||
// Nombres del tema (para mostrar "SOURCE → TARGET" durante transición)
|
||||
std::string name_en;
|
||||
std::string name_es;
|
||||
// Nombres del tema (para mostrar "SOURCE → TARGET" durante transición)
|
||||
std::string name_en;
|
||||
std::string name_es;
|
||||
|
||||
// Colores de pelotas capturados (índice = ball_index % ball_colors.size())
|
||||
// Se capturan suficientes colores para cubrir escenario máximo (50,000 pelotas)
|
||||
// Nota: Si el tema tiene 8 colores y capturamos 50,000, habrá repetición
|
||||
// pero permite LERP correcto incluso con muchas pelotas
|
||||
std::vector<Color> ball_colors;
|
||||
// Colores de pelotas capturados (índice = ball_index % ball_colors.size())
|
||||
// Se capturan suficientes colores para cubrir escenario máximo (50,000 pelotas)
|
||||
// Nota: Si el tema tiene 8 colores y capturamos 50,000, habrá repetición
|
||||
// pero permite LERP correcto incluso con muchas pelotas
|
||||
std::vector<Color> ball_colors;
|
||||
};
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
#include "app_logo.hpp"
|
||||
|
||||
#include <SDL3/SDL_render.h> // for SDL_DestroyTexture, SDL_RenderGeometry, SDL_SetTextureAlphaMod
|
||||
#include <cmath> // for powf, sinf, cosf
|
||||
#include <cstdlib> // for free()
|
||||
#include <iostream> // for std::cout
|
||||
|
||||
#include "logo_scaler.hpp" // for LogoScaler
|
||||
#include "defines.hpp" // for APPLOGO_HEIGHT_PERCENT, getResourcesDirectory
|
||||
#include <array> // for std::array
|
||||
#include <cmath> // for powf, sinf, cosf
|
||||
#include <cstdlib> // for free()
|
||||
#include <iostream> // for std::cout
|
||||
#include <numbers>
|
||||
|
||||
#include "defines.hpp" // for APPLOGO_HEIGHT_PERCENT, getResourcesDirectory
|
||||
#include "logo_scaler.hpp" // for LogoScaler
|
||||
|
||||
// ============================================================================
|
||||
// Destructor - Liberar las 4 texturas SDL
|
||||
// ============================================================================
|
||||
|
||||
AppLogo::~AppLogo() {
|
||||
if (logo1_base_texture_) {
|
||||
if (logo1_base_texture_ != nullptr) {
|
||||
SDL_DestroyTexture(logo1_base_texture_);
|
||||
logo1_base_texture_ = nullptr;
|
||||
}
|
||||
if (logo1_native_texture_) {
|
||||
if (logo1_native_texture_ != nullptr) {
|
||||
SDL_DestroyTexture(logo1_native_texture_);
|
||||
logo1_native_texture_ = nullptr;
|
||||
}
|
||||
if (logo2_base_texture_) {
|
||||
if (logo2_base_texture_ != nullptr) {
|
||||
SDL_DestroyTexture(logo2_base_texture_);
|
||||
logo2_base_texture_ = nullptr;
|
||||
}
|
||||
if (logo2_native_texture_) {
|
||||
if (logo2_native_texture_ != nullptr) {
|
||||
SDL_DestroyTexture(logo2_native_texture_);
|
||||
logo2_native_texture_ = nullptr;
|
||||
}
|
||||
@@ -35,11 +38,23 @@ AppLogo::~AppLogo() {
|
||||
// Inicialización - Pre-escalar logos a 2 resoluciones (base y nativa)
|
||||
// ============================================================================
|
||||
|
||||
bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_height) {
|
||||
if (logo1_base_texture_) { SDL_DestroyTexture(logo1_base_texture_); logo1_base_texture_ = nullptr; }
|
||||
if (logo1_native_texture_) { SDL_DestroyTexture(logo1_native_texture_); logo1_native_texture_ = nullptr; }
|
||||
if (logo2_base_texture_) { SDL_DestroyTexture(logo2_base_texture_); logo2_base_texture_ = nullptr; }
|
||||
if (logo2_native_texture_) { SDL_DestroyTexture(logo2_native_texture_); logo2_native_texture_ = nullptr; }
|
||||
auto AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_height) -> bool {
|
||||
if (logo1_base_texture_ != nullptr) {
|
||||
SDL_DestroyTexture(logo1_base_texture_);
|
||||
logo1_base_texture_ = nullptr;
|
||||
}
|
||||
if (logo1_native_texture_ != nullptr) {
|
||||
SDL_DestroyTexture(logo1_native_texture_);
|
||||
logo1_native_texture_ = nullptr;
|
||||
}
|
||||
if (logo2_base_texture_ != nullptr) {
|
||||
SDL_DestroyTexture(logo2_base_texture_);
|
||||
logo2_base_texture_ = nullptr;
|
||||
}
|
||||
if (logo2_native_texture_ != nullptr) {
|
||||
SDL_DestroyTexture(logo2_native_texture_);
|
||||
logo2_native_texture_ = nullptr;
|
||||
}
|
||||
|
||||
renderer_ = renderer;
|
||||
base_screen_width_ = screen_width;
|
||||
@@ -53,7 +68,7 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
|
||||
// 1. Detectar resolución nativa del monitor
|
||||
// ========================================================================
|
||||
if (!LogoScaler::detectNativeResolution(native_screen_width_, native_screen_height_)) {
|
||||
std::cout << "No se pudo detectar resolución nativa, usando solo base" << std::endl;
|
||||
std::cout << "No se pudo detectar resolución nativa, usando solo base" << '\n';
|
||||
// Fallback: usar resolución base como nativa
|
||||
native_screen_width_ = screen_width;
|
||||
native_screen_height_ = screen_height;
|
||||
@@ -65,12 +80,6 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
|
||||
int logo_base_target_height = static_cast<int>(base_screen_height_ * APPLOGO_HEIGHT_PERCENT);
|
||||
int logo_native_target_height = static_cast<int>(native_screen_height_ * APPLOGO_HEIGHT_PERCENT);
|
||||
|
||||
std::cout << "Pre-escalando logos:" << std::endl;
|
||||
std::cout << " Base: " << base_screen_width_ << "x" << base_screen_height_
|
||||
<< " (altura logo: " << logo_base_target_height << "px)" << std::endl;
|
||||
std::cout << " Nativa: " << native_screen_width_ << "x" << native_screen_height_
|
||||
<< " (altura logo: " << logo_native_target_height << "px)" << std::endl;
|
||||
|
||||
// ========================================================================
|
||||
// 3. Cargar y escalar LOGO1 (data/logo/logo.png) a 2 versiones
|
||||
// ========================================================================
|
||||
@@ -82,20 +91,21 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
|
||||
0, // width calculado automáticamente por aspect ratio
|
||||
logo_base_target_height,
|
||||
logo1_base_width_,
|
||||
logo1_base_height_
|
||||
);
|
||||
logo1_base_height_);
|
||||
if (logo1_base_data == nullptr) {
|
||||
std::cout << "Error: No se pudo escalar logo1 (base)" << std::endl;
|
||||
std::cout << "Error: No se pudo escalar logo1 (base)" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
logo1_base_texture_ = LogoScaler::createTextureFromBuffer(
|
||||
renderer, logo1_base_data, logo1_base_width_, logo1_base_height_
|
||||
);
|
||||
renderer,
|
||||
logo1_base_data,
|
||||
logo1_base_width_,
|
||||
logo1_base_height_);
|
||||
free(logo1_base_data); // Liberar buffer temporal
|
||||
|
||||
if (logo1_base_texture_ == nullptr) {
|
||||
std::cout << "Error: No se pudo crear textura logo1 (base)" << std::endl;
|
||||
std::cout << "Error: No se pudo crear textura logo1 (base)" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -108,20 +118,21 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
|
||||
0, // width calculado automáticamente
|
||||
logo_native_target_height,
|
||||
logo1_native_width_,
|
||||
logo1_native_height_
|
||||
);
|
||||
logo1_native_height_);
|
||||
if (logo1_native_data == nullptr) {
|
||||
std::cout << "Error: No se pudo escalar logo1 (nativa)" << std::endl;
|
||||
std::cout << "Error: No se pudo escalar logo1 (nativa)" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
logo1_native_texture_ = LogoScaler::createTextureFromBuffer(
|
||||
renderer, logo1_native_data, logo1_native_width_, logo1_native_height_
|
||||
);
|
||||
renderer,
|
||||
logo1_native_data,
|
||||
logo1_native_width_,
|
||||
logo1_native_height_);
|
||||
free(logo1_native_data);
|
||||
|
||||
if (logo1_native_texture_ == nullptr) {
|
||||
std::cout << "Error: No se pudo crear textura logo1 (nativa)" << std::endl;
|
||||
std::cout << "Error: No se pudo crear textura logo1 (nativa)" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -138,20 +149,21 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
|
||||
0,
|
||||
logo_base_target_height,
|
||||
logo2_base_width_,
|
||||
logo2_base_height_
|
||||
);
|
||||
logo2_base_height_);
|
||||
if (logo2_base_data == nullptr) {
|
||||
std::cout << "Error: No se pudo escalar logo2 (base)" << std::endl;
|
||||
std::cout << "Error: No se pudo escalar logo2 (base)" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
logo2_base_texture_ = LogoScaler::createTextureFromBuffer(
|
||||
renderer, logo2_base_data, logo2_base_width_, logo2_base_height_
|
||||
);
|
||||
renderer,
|
||||
logo2_base_data,
|
||||
logo2_base_width_,
|
||||
logo2_base_height_);
|
||||
free(logo2_base_data);
|
||||
|
||||
if (logo2_base_texture_ == nullptr) {
|
||||
std::cout << "Error: No se pudo crear textura logo2 (base)" << std::endl;
|
||||
std::cout << "Error: No se pudo crear textura logo2 (base)" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -163,20 +175,21 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
|
||||
0,
|
||||
logo_native_target_height,
|
||||
logo2_native_width_,
|
||||
logo2_native_height_
|
||||
);
|
||||
logo2_native_height_);
|
||||
if (logo2_native_data == nullptr) {
|
||||
std::cout << "Error: No se pudo escalar logo2 (nativa)" << std::endl;
|
||||
std::cout << "Error: No se pudo escalar logo2 (nativa)" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
logo2_native_texture_ = LogoScaler::createTextureFromBuffer(
|
||||
renderer, logo2_native_data, logo2_native_width_, logo2_native_height_
|
||||
);
|
||||
renderer,
|
||||
logo2_native_data,
|
||||
logo2_native_width_,
|
||||
logo2_native_height_);
|
||||
free(logo2_native_data);
|
||||
|
||||
if (logo2_native_texture_ == nullptr) {
|
||||
std::cout << "Error: No se pudo crear textura logo2 (nativa)" << std::endl;
|
||||
std::cout << "Error: No se pudo crear textura logo2 (nativa)" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -193,11 +206,11 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
|
||||
logo2_current_width_ = logo2_base_width_;
|
||||
logo2_current_height_ = logo2_base_height_;
|
||||
|
||||
std::cout << "Logos pre-escalados exitosamente (4 texturas creadas)" << std::endl;
|
||||
std::cout << "[Logo] logo.png + logo2.png (base " << logo_base_target_height << "px, nativa " << logo_native_target_height << "px)\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
void AppLogo::update(float delta_time, AppMode current_mode) {
|
||||
void AppLogo::update(float delta_time, AppMode current_mode) { // NOLINT(readability-function-cognitive-complexity)
|
||||
// Si estamos en SANDBOX, resetear y no hacer nada (logo desactivado)
|
||||
if (current_mode == AppMode::SANDBOX) {
|
||||
state_ = AppLogoState::HIDDEN;
|
||||
@@ -268,63 +281,57 @@ void AppLogo::update(float delta_time, AppMode current_mode) {
|
||||
logo2_rotation_ = 0.0f;
|
||||
break;
|
||||
|
||||
case AppLogoAnimationType::ELASTIC_STICK:
|
||||
{
|
||||
float prog1 = std::min(1.0f, fade_progress_logo1);
|
||||
float elastic_t1 = easeOutElastic(prog1);
|
||||
logo1_scale_ = 1.2f - (elastic_t1 * 0.2f);
|
||||
float squash_t1 = easeOutBack(prog1);
|
||||
logo1_squash_y_ = 0.6f + (squash_t1 * 0.4f);
|
||||
logo1_stretch_x_ = 1.0f + (1.0f - logo1_squash_y_) * 0.5f;
|
||||
logo1_rotation_ = 0.0f;
|
||||
case AppLogoAnimationType::ELASTIC_STICK: {
|
||||
float prog1 = std::min(1.0f, fade_progress_logo1);
|
||||
float elastic_t1 = easeOutElastic(prog1);
|
||||
logo1_scale_ = 1.2f - (elastic_t1 * 0.2f);
|
||||
float squash_t1 = easeOutBack(prog1);
|
||||
logo1_squash_y_ = 0.6f + (squash_t1 * 0.4f);
|
||||
logo1_stretch_x_ = 1.0f + (1.0f - logo1_squash_y_) * 0.5f;
|
||||
logo1_rotation_ = 0.0f;
|
||||
|
||||
float prog2 = std::min(1.0f, fade_progress_logo2);
|
||||
float elastic_t2 = easeOutElastic(prog2);
|
||||
logo2_scale_ = 1.2f - (elastic_t2 * 0.2f);
|
||||
float squash_t2 = easeOutBack(prog2);
|
||||
logo2_squash_y_ = 0.6f + (squash_t2 * 0.4f);
|
||||
logo2_stretch_x_ = 1.0f + (1.0f - logo2_squash_y_) * 0.5f;
|
||||
logo2_rotation_ = 0.0f;
|
||||
}
|
||||
break;
|
||||
float prog2 = std::min(1.0f, fade_progress_logo2);
|
||||
float elastic_t2 = easeOutElastic(prog2);
|
||||
logo2_scale_ = 1.2f - (elastic_t2 * 0.2f);
|
||||
float squash_t2 = easeOutBack(prog2);
|
||||
logo2_squash_y_ = 0.6f + (squash_t2 * 0.4f);
|
||||
logo2_stretch_x_ = 1.0f + (1.0f - logo2_squash_y_) * 0.5f;
|
||||
logo2_rotation_ = 0.0f;
|
||||
} break;
|
||||
|
||||
case AppLogoAnimationType::ROTATE_SPIRAL:
|
||||
{
|
||||
float prog1 = std::min(1.0f, fade_progress_logo1);
|
||||
float ease_t1 = easeInOutQuad(prog1);
|
||||
logo1_scale_ = 0.3f + (ease_t1 * 0.7f);
|
||||
logo1_rotation_ = (1.0f - prog1) * 6.28f;
|
||||
logo1_squash_y_ = 1.0f;
|
||||
logo1_stretch_x_ = 1.0f;
|
||||
case AppLogoAnimationType::ROTATE_SPIRAL: {
|
||||
float prog1 = std::min(1.0f, fade_progress_logo1);
|
||||
float ease_t1 = easeInOutQuad(prog1);
|
||||
logo1_scale_ = 0.3f + (ease_t1 * 0.7f);
|
||||
logo1_rotation_ = (1.0f - prog1) * 6.28f;
|
||||
logo1_squash_y_ = 1.0f;
|
||||
logo1_stretch_x_ = 1.0f;
|
||||
|
||||
float prog2 = std::min(1.0f, fade_progress_logo2);
|
||||
float ease_t2 = easeInOutQuad(prog2);
|
||||
logo2_scale_ = 0.3f + (ease_t2 * 0.7f);
|
||||
logo2_rotation_ = (1.0f - prog2) * 6.28f;
|
||||
logo2_squash_y_ = 1.0f;
|
||||
logo2_stretch_x_ = 1.0f;
|
||||
}
|
||||
break;
|
||||
float prog2 = std::min(1.0f, fade_progress_logo2);
|
||||
float ease_t2 = easeInOutQuad(prog2);
|
||||
logo2_scale_ = 0.3f + (ease_t2 * 0.7f);
|
||||
logo2_rotation_ = (1.0f - prog2) * 6.28f;
|
||||
logo2_squash_y_ = 1.0f;
|
||||
logo2_stretch_x_ = 1.0f;
|
||||
} break;
|
||||
|
||||
case AppLogoAnimationType::BOUNCE_SQUASH:
|
||||
{
|
||||
float prog1 = std::min(1.0f, fade_progress_logo1);
|
||||
float bounce_t1 = easeOutBounce(prog1);
|
||||
logo1_scale_ = 1.0f;
|
||||
float squash_amount1 = (1.0f - bounce_t1) * 0.3f;
|
||||
logo1_squash_y_ = 1.0f - squash_amount1;
|
||||
logo1_stretch_x_ = 1.0f + squash_amount1 * 0.5f;
|
||||
logo1_rotation_ = 0.0f;
|
||||
case AppLogoAnimationType::BOUNCE_SQUASH: {
|
||||
float prog1 = std::min(1.0f, fade_progress_logo1);
|
||||
float bounce_t1 = easeOutBounce(prog1);
|
||||
logo1_scale_ = 1.0f;
|
||||
float squash_amount1 = (1.0f - bounce_t1) * 0.3f;
|
||||
logo1_squash_y_ = 1.0f - squash_amount1;
|
||||
logo1_stretch_x_ = 1.0f + squash_amount1 * 0.5f;
|
||||
logo1_rotation_ = 0.0f;
|
||||
|
||||
float prog2 = std::min(1.0f, fade_progress_logo2);
|
||||
float bounce_t2 = easeOutBounce(prog2);
|
||||
logo2_scale_ = 1.0f;
|
||||
float squash_amount2 = (1.0f - bounce_t2) * 0.3f;
|
||||
logo2_squash_y_ = 1.0f - squash_amount2;
|
||||
logo2_stretch_x_ = 1.0f + squash_amount2 * 0.5f;
|
||||
logo2_rotation_ = 0.0f;
|
||||
}
|
||||
break;
|
||||
float prog2 = std::min(1.0f, fade_progress_logo2);
|
||||
float bounce_t2 = easeOutBounce(prog2);
|
||||
logo2_scale_ = 1.0f;
|
||||
float squash_amount2 = (1.0f - bounce_t2) * 0.3f;
|
||||
logo2_squash_y_ = 1.0f - squash_amount2;
|
||||
logo2_stretch_x_ = 1.0f + squash_amount2 * 0.5f;
|
||||
logo2_rotation_ = 0.0f;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,69 +392,63 @@ void AppLogo::update(float delta_time, AppMode current_mode) {
|
||||
logo2_rotation_ = 0.0f;
|
||||
break;
|
||||
|
||||
case AppLogoAnimationType::ELASTIC_STICK:
|
||||
{
|
||||
float prog1 = std::min(1.0f, fade_progress_logo1);
|
||||
logo1_scale_ = 1.0f + (prog1 * prog1 * 0.2f);
|
||||
logo1_squash_y_ = 1.0f + (prog1 * 0.3f);
|
||||
logo1_stretch_x_ = 1.0f - (prog1 * 0.2f);
|
||||
logo1_rotation_ = prog1 * 0.1f;
|
||||
case AppLogoAnimationType::ELASTIC_STICK: {
|
||||
float prog1 = std::min(1.0f, fade_progress_logo1);
|
||||
logo1_scale_ = 1.0f + (prog1 * prog1 * 0.2f);
|
||||
logo1_squash_y_ = 1.0f + (prog1 * 0.3f);
|
||||
logo1_stretch_x_ = 1.0f - (prog1 * 0.2f);
|
||||
logo1_rotation_ = prog1 * 0.1f;
|
||||
|
||||
float prog2 = std::min(1.0f, fade_progress_logo2);
|
||||
logo2_scale_ = 1.0f + (prog2 * prog2 * 0.2f);
|
||||
logo2_squash_y_ = 1.0f + (prog2 * 0.3f);
|
||||
logo2_stretch_x_ = 1.0f - (prog2 * 0.2f);
|
||||
logo2_rotation_ = prog2 * 0.1f;
|
||||
float prog2 = std::min(1.0f, fade_progress_logo2);
|
||||
logo2_scale_ = 1.0f + (prog2 * prog2 * 0.2f);
|
||||
logo2_squash_y_ = 1.0f + (prog2 * 0.3f);
|
||||
logo2_stretch_x_ = 1.0f - (prog2 * 0.2f);
|
||||
logo2_rotation_ = prog2 * 0.1f;
|
||||
} break;
|
||||
|
||||
case AppLogoAnimationType::ROTATE_SPIRAL: {
|
||||
float prog1 = std::min(1.0f, fade_progress_logo1);
|
||||
float ease_t1 = easeInOutQuad(prog1);
|
||||
logo1_scale_ = 1.0f - (ease_t1 * 0.7f);
|
||||
logo1_rotation_ = prog1 * 6.28f;
|
||||
logo1_squash_y_ = 1.0f;
|
||||
logo1_stretch_x_ = 1.0f;
|
||||
|
||||
float prog2 = std::min(1.0f, fade_progress_logo2);
|
||||
float ease_t2 = easeInOutQuad(prog2);
|
||||
logo2_scale_ = 1.0f - (ease_t2 * 0.7f);
|
||||
logo2_rotation_ = prog2 * 6.28f;
|
||||
logo2_squash_y_ = 1.0f;
|
||||
logo2_stretch_x_ = 1.0f;
|
||||
} break;
|
||||
|
||||
case AppLogoAnimationType::BOUNCE_SQUASH: {
|
||||
float prog1 = std::min(1.0f, fade_progress_logo1);
|
||||
if (prog1 < 0.2f) {
|
||||
float squash_t = prog1 / 0.2f;
|
||||
logo1_squash_y_ = 1.0f - (squash_t * 0.3f);
|
||||
logo1_stretch_x_ = 1.0f + (squash_t * 0.2f);
|
||||
} else {
|
||||
float jump_t = (prog1 - 0.2f) / 0.8f;
|
||||
logo1_squash_y_ = 0.7f + (jump_t * 0.5f);
|
||||
logo1_stretch_x_ = 1.2f - (jump_t * 0.2f);
|
||||
}
|
||||
break;
|
||||
logo1_scale_ = 1.0f + (prog1 * 0.3f);
|
||||
logo1_rotation_ = 0.0f;
|
||||
|
||||
case AppLogoAnimationType::ROTATE_SPIRAL:
|
||||
{
|
||||
float prog1 = std::min(1.0f, fade_progress_logo1);
|
||||
float ease_t1 = easeInOutQuad(prog1);
|
||||
logo1_scale_ = 1.0f - (ease_t1 * 0.7f);
|
||||
logo1_rotation_ = prog1 * 6.28f;
|
||||
logo1_squash_y_ = 1.0f;
|
||||
logo1_stretch_x_ = 1.0f;
|
||||
|
||||
float prog2 = std::min(1.0f, fade_progress_logo2);
|
||||
float ease_t2 = easeInOutQuad(prog2);
|
||||
logo2_scale_ = 1.0f - (ease_t2 * 0.7f);
|
||||
logo2_rotation_ = prog2 * 6.28f;
|
||||
logo2_squash_y_ = 1.0f;
|
||||
logo2_stretch_x_ = 1.0f;
|
||||
float prog2 = std::min(1.0f, fade_progress_logo2);
|
||||
if (prog2 < 0.2f) {
|
||||
float squash_t = prog2 / 0.2f;
|
||||
logo2_squash_y_ = 1.0f - (squash_t * 0.3f);
|
||||
logo2_stretch_x_ = 1.0f + (squash_t * 0.2f);
|
||||
} else {
|
||||
float jump_t = (prog2 - 0.2f) / 0.8f;
|
||||
logo2_squash_y_ = 0.7f + (jump_t * 0.5f);
|
||||
logo2_stretch_x_ = 1.2f - (jump_t * 0.2f);
|
||||
}
|
||||
break;
|
||||
|
||||
case AppLogoAnimationType::BOUNCE_SQUASH:
|
||||
{
|
||||
float prog1 = std::min(1.0f, fade_progress_logo1);
|
||||
if (prog1 < 0.2f) {
|
||||
float squash_t = prog1 / 0.2f;
|
||||
logo1_squash_y_ = 1.0f - (squash_t * 0.3f);
|
||||
logo1_stretch_x_ = 1.0f + (squash_t * 0.2f);
|
||||
} else {
|
||||
float jump_t = (prog1 - 0.2f) / 0.8f;
|
||||
logo1_squash_y_ = 0.7f + (jump_t * 0.5f);
|
||||
logo1_stretch_x_ = 1.2f - (jump_t * 0.2f);
|
||||
}
|
||||
logo1_scale_ = 1.0f + (prog1 * 0.3f);
|
||||
logo1_rotation_ = 0.0f;
|
||||
|
||||
float prog2 = std::min(1.0f, fade_progress_logo2);
|
||||
if (prog2 < 0.2f) {
|
||||
float squash_t = prog2 / 0.2f;
|
||||
logo2_squash_y_ = 1.0f - (squash_t * 0.3f);
|
||||
logo2_stretch_x_ = 1.0f + (squash_t * 0.2f);
|
||||
} else {
|
||||
float jump_t = (prog2 - 0.2f) / 0.8f;
|
||||
logo2_squash_y_ = 0.7f + (jump_t * 0.5f);
|
||||
logo2_stretch_x_ = 1.2f - (jump_t * 0.2f);
|
||||
}
|
||||
logo2_scale_ = 1.0f + (prog2 * 0.3f);
|
||||
logo2_rotation_ = 0.0f;
|
||||
}
|
||||
break;
|
||||
logo2_scale_ = 1.0f + (prog2 * 0.3f);
|
||||
logo2_rotation_ = 0.0f;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -483,7 +484,7 @@ void AppLogo::updateScreenSize(int screen_width, int screen_height) {
|
||||
logo2_current_width_ = logo2_native_width_;
|
||||
logo2_current_height_ = logo2_native_height_;
|
||||
|
||||
std::cout << "AppLogo: Cambiado a texturas NATIVAS" << std::endl;
|
||||
std::cout << "AppLogo: Cambiado a texturas NATIVAS" << '\n';
|
||||
} else {
|
||||
// Cambiar a texturas base (ventana redimensionable)
|
||||
logo1_current_texture_ = logo1_base_texture_;
|
||||
@@ -494,7 +495,7 @@ void AppLogo::updateScreenSize(int screen_width, int screen_height) {
|
||||
logo2_current_width_ = logo2_base_width_;
|
||||
logo2_current_height_ = logo2_base_height_;
|
||||
|
||||
std::cout << "AppLogo: Cambiado a texturas BASE" << std::endl;
|
||||
std::cout << "AppLogo: Cambiado a texturas BASE" << '\n';
|
||||
}
|
||||
|
||||
// Nota: No es necesario recalcular escalas porque las texturas están pre-escaladas
|
||||
@@ -505,57 +506,61 @@ void AppLogo::updateScreenSize(int screen_width, int screen_height) {
|
||||
// Funciones de easing para animaciones
|
||||
// ============================================================================
|
||||
|
||||
float AppLogo::easeOutElastic(float t) {
|
||||
auto AppLogo::easeOutElastic(float t) -> float {
|
||||
// Elastic easing out: bounce elástico al final
|
||||
const float c4 = (2.0f * 3.14159f) / 3.0f;
|
||||
const float C4 = (2.0f * std::numbers::pi_v<float>) / 3.0f;
|
||||
|
||||
if (t == 0.0f) return 0.0f;
|
||||
if (t == 1.0f) return 1.0f;
|
||||
|
||||
return powf(2.0f, -10.0f * t) * sinf((t * 10.0f - 0.75f) * c4) + 1.0f;
|
||||
}
|
||||
|
||||
float AppLogo::easeOutBack(float t) {
|
||||
// Back easing out: overshoot suave al final
|
||||
const float c1 = 1.70158f;
|
||||
const float c3 = c1 + 1.0f;
|
||||
|
||||
return 1.0f + c3 * powf(t - 1.0f, 3.0f) + c1 * powf(t - 1.0f, 2.0f);
|
||||
}
|
||||
|
||||
float AppLogo::easeOutBounce(float t) {
|
||||
// Bounce easing out: rebotes decrecientes (para BOUNCE_SQUASH)
|
||||
const float n1 = 7.5625f;
|
||||
const float d1 = 2.75f;
|
||||
|
||||
if (t < 1.0f / d1) {
|
||||
return n1 * t * t;
|
||||
} else if (t < 2.0f / d1) {
|
||||
t -= 1.5f / d1;
|
||||
return n1 * t * t + 0.75f;
|
||||
} else if (t < 2.5f / d1) {
|
||||
t -= 2.25f / d1;
|
||||
return n1 * t * t + 0.9375f;
|
||||
} else {
|
||||
t -= 2.625f / d1;
|
||||
return n1 * t * t + 0.984375f;
|
||||
if (t == 0.0f) {
|
||||
return 0.0f;
|
||||
}
|
||||
if (t == 1.0f) {
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
return (powf(2.0f, -10.0f * t) * sinf((t * 10.0f - 0.75f) * C4)) + 1.0f;
|
||||
}
|
||||
|
||||
float AppLogo::easeInOutQuad(float t) {
|
||||
auto AppLogo::easeOutBack(float t) -> float {
|
||||
// Back easing out: overshoot suave al final
|
||||
const float C1 = 1.70158f;
|
||||
const float C3 = C1 + 1.0f;
|
||||
|
||||
return 1.0f + (C3 * powf(t - 1.0f, 3.0f)) + (C1 * powf(t - 1.0f, 2.0f));
|
||||
}
|
||||
|
||||
auto AppLogo::easeOutBounce(float t) -> float {
|
||||
// Bounce easing out: rebotes decrecientes (para BOUNCE_SQUASH)
|
||||
const float N1 = 7.5625f;
|
||||
const float D1 = 2.75f;
|
||||
|
||||
if (t < 1.0f / D1) {
|
||||
return N1 * t * t;
|
||||
}
|
||||
if (t < 2.0f / D1) {
|
||||
t -= 1.5f / D1;
|
||||
return (N1 * t * t) + 0.75f;
|
||||
}
|
||||
if (t < 2.5f / D1) {
|
||||
t -= 2.25f / D1;
|
||||
return (N1 * t * t) + 0.9375f;
|
||||
}
|
||||
t -= 2.625f / D1;
|
||||
return (N1 * t * t) + 0.984375f;
|
||||
}
|
||||
|
||||
auto AppLogo::easeInOutQuad(float t) -> float {
|
||||
// Quadratic easing in/out: aceleración suave (para ROTATE_SPIRAL)
|
||||
if (t < 0.5f) {
|
||||
return 2.0f * t * t;
|
||||
} else {
|
||||
return 1.0f - powf(-2.0f * t + 2.0f, 2.0f) / 2.0f;
|
||||
}
|
||||
return 1.0f - (powf((-2.0f * t) + 2.0f, 2.0f) / 2.0f);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Función auxiliar para aleatorización
|
||||
// ============================================================================
|
||||
|
||||
AppLogoAnimationType AppLogo::getRandomAnimation() {
|
||||
auto AppLogo::getRandomAnimation() -> AppLogoAnimationType {
|
||||
// Generar número aleatorio entre 0 y 3 (4 tipos de animación)
|
||||
int random_value = rand() % 4;
|
||||
|
||||
@@ -577,15 +582,23 @@ AppLogoAnimationType AppLogo::getRandomAnimation() {
|
||||
// ============================================================================
|
||||
|
||||
void AppLogo::renderWithGeometry(int logo_index) {
|
||||
if (!renderer_) return;
|
||||
if (renderer_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Seleccionar variables según el logo_index (1 = logo1, 2 = logo2)
|
||||
SDL_Texture* texture;
|
||||
int base_width, base_height;
|
||||
float scale, squash_y, stretch_x, rotation;
|
||||
int base_width;
|
||||
int base_height;
|
||||
float scale;
|
||||
float squash_y;
|
||||
float stretch_x;
|
||||
float rotation;
|
||||
|
||||
if (logo_index == 1) {
|
||||
if (!logo1_current_texture_) return;
|
||||
if (logo1_current_texture_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
texture = logo1_current_texture_;
|
||||
base_width = logo1_current_width_;
|
||||
base_height = logo1_current_height_;
|
||||
@@ -594,7 +607,9 @@ void AppLogo::renderWithGeometry(int logo_index) {
|
||||
stretch_x = logo1_stretch_x_;
|
||||
rotation = logo1_rotation_;
|
||||
} else if (logo_index == 2) {
|
||||
if (!logo2_current_texture_) return;
|
||||
if (logo2_current_texture_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
texture = logo2_current_texture_;
|
||||
base_width = logo2_current_width_;
|
||||
base_height = logo2_current_height_;
|
||||
@@ -634,7 +649,7 @@ void AppLogo::renderWithGeometry(int logo_index) {
|
||||
float sin_rot = sinf(rotation);
|
||||
|
||||
// Crear 4 vértices del quad (centrado en center_x, center_y)
|
||||
SDL_Vertex vertices[4];
|
||||
std::array<SDL_Vertex, 4> vertices{};
|
||||
|
||||
// Offset desde el centro
|
||||
float half_w = width / 2.0f;
|
||||
@@ -644,49 +659,49 @@ void AppLogo::renderWithGeometry(int logo_index) {
|
||||
{
|
||||
float local_x = -half_w;
|
||||
float local_y = -half_h;
|
||||
float rotated_x = local_x * cos_rot - local_y * sin_rot;
|
||||
float rotated_y = local_x * sin_rot + local_y * cos_rot;
|
||||
vertices[0].position = {center_x + rotated_x, center_y + rotated_y};
|
||||
vertices[0].tex_coord = {0.0f, 0.0f};
|
||||
vertices[0].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice
|
||||
float rotated_x = (local_x * cos_rot) - (local_y * sin_rot);
|
||||
float rotated_y = (local_x * sin_rot) + (local_y * cos_rot);
|
||||
vertices[0].position = {.x = center_x + rotated_x, .y = center_y + rotated_y};
|
||||
vertices[0].tex_coord = {.x = 0.0f, .y = 0.0f};
|
||||
vertices[0].color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = alpha_normalized}; // Alpha aplicado al vértice
|
||||
}
|
||||
|
||||
// Vértice superior derecho (rotado)
|
||||
{
|
||||
float local_x = half_w;
|
||||
float local_y = -half_h;
|
||||
float rotated_x = local_x * cos_rot - local_y * sin_rot;
|
||||
float rotated_y = local_x * sin_rot + local_y * cos_rot;
|
||||
vertices[1].position = {center_x + rotated_x, center_y + rotated_y};
|
||||
vertices[1].tex_coord = {1.0f, 0.0f};
|
||||
vertices[1].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice
|
||||
float rotated_x = (local_x * cos_rot) - (local_y * sin_rot);
|
||||
float rotated_y = (local_x * sin_rot) + (local_y * cos_rot);
|
||||
vertices[1].position = {.x = center_x + rotated_x, .y = center_y + rotated_y};
|
||||
vertices[1].tex_coord = {.x = 1.0f, .y = 0.0f};
|
||||
vertices[1].color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = alpha_normalized}; // Alpha aplicado al vértice
|
||||
}
|
||||
|
||||
// Vértice inferior derecho (rotado)
|
||||
{
|
||||
float local_x = half_w;
|
||||
float local_y = half_h;
|
||||
float rotated_x = local_x * cos_rot - local_y * sin_rot;
|
||||
float rotated_y = local_x * sin_rot + local_y * cos_rot;
|
||||
vertices[2].position = {center_x + rotated_x, center_y + rotated_y};
|
||||
vertices[2].tex_coord = {1.0f, 1.0f};
|
||||
vertices[2].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice
|
||||
float rotated_x = (local_x * cos_rot) - (local_y * sin_rot);
|
||||
float rotated_y = (local_x * sin_rot) + (local_y * cos_rot);
|
||||
vertices[2].position = {.x = center_x + rotated_x, .y = center_y + rotated_y};
|
||||
vertices[2].tex_coord = {.x = 1.0f, .y = 1.0f};
|
||||
vertices[2].color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = alpha_normalized}; // Alpha aplicado al vértice
|
||||
}
|
||||
|
||||
// Vértice inferior izquierdo (rotado)
|
||||
{
|
||||
float local_x = -half_w;
|
||||
float local_y = half_h;
|
||||
float rotated_x = local_x * cos_rot - local_y * sin_rot;
|
||||
float rotated_y = local_x * sin_rot + local_y * cos_rot;
|
||||
vertices[3].position = {center_x + rotated_x, center_y + rotated_y};
|
||||
vertices[3].tex_coord = {0.0f, 1.0f};
|
||||
vertices[3].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice
|
||||
float rotated_x = (local_x * cos_rot) - (local_y * sin_rot);
|
||||
float rotated_y = (local_x * sin_rot) + (local_y * cos_rot);
|
||||
vertices[3].position = {.x = center_x + rotated_x, .y = center_y + rotated_y};
|
||||
vertices[3].tex_coord = {.x = 0.0f, .y = 1.0f};
|
||||
vertices[3].color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = alpha_normalized}; // Alpha aplicado al vértice
|
||||
}
|
||||
|
||||
// Índices para 2 triángulos
|
||||
int indices[6] = {0, 1, 2, 2, 3, 0};
|
||||
std::array<int, 6> indices = {0, 1, 2, 2, 3, 0};
|
||||
|
||||
// Renderizar con la textura del logo correspondiente
|
||||
SDL_RenderGeometry(renderer_, texture, vertices, 4, indices, 6);
|
||||
SDL_RenderGeometry(renderer_, texture, vertices.data(), 4, indices.data(), 6);
|
||||
}
|
||||
|
||||
@@ -11,18 +11,18 @@ class Sprite;
|
||||
|
||||
// Estados de la máquina de estados del logo
|
||||
enum class AppLogoState {
|
||||
HIDDEN, // Logo oculto, esperando APPLOGO_DISPLAY_INTERVAL
|
||||
FADE_IN, // Apareciendo (alpha 0 → 255)
|
||||
VISIBLE, // Completamente visible, esperando APPLOGO_DISPLAY_DURATION
|
||||
FADE_OUT // Desapareciendo (alpha 255 → 0)
|
||||
HIDDEN, // Logo oculto, esperando APPLOGO_DISPLAY_INTERVAL
|
||||
FADE_IN, // Apareciendo (alpha 0 → 255)
|
||||
VISIBLE, // Completamente visible, esperando APPLOGO_DISPLAY_DURATION
|
||||
FADE_OUT // Desapareciendo (alpha 255 → 0)
|
||||
};
|
||||
|
||||
// Tipo de animación de entrada/salida
|
||||
enum class AppLogoAnimationType {
|
||||
ZOOM_ONLY, // A: Solo zoom simple (120% → 100% → 120%)
|
||||
ELASTIC_STICK, // B: Zoom + deformación elástica tipo "pegatina"
|
||||
ROTATE_SPIRAL, // C: Rotación en espiral (entra girando, sale girando)
|
||||
BOUNCE_SQUASH // D: Rebote con aplastamiento (cae rebotando, salta)
|
||||
ZOOM_ONLY, // A: Solo zoom simple (120% → 100% → 120%)
|
||||
ELASTIC_STICK, // B: Zoom + deformación elástica tipo "pegatina"
|
||||
ROTATE_SPIRAL, // C: Rotación en espiral (entra girando, sale girando)
|
||||
BOUNCE_SQUASH // D: Rebote con aplastamiento (cae rebotando, salta)
|
||||
};
|
||||
|
||||
class AppLogo {
|
||||
@@ -46,10 +46,10 @@ class AppLogo {
|
||||
// ====================================================================
|
||||
// Texturas pre-escaladas (4 texturas: 2 logos × 2 resoluciones)
|
||||
// ====================================================================
|
||||
SDL_Texture* logo1_base_texture_ = nullptr; // Logo1 para resolución base
|
||||
SDL_Texture* logo1_native_texture_ = nullptr; // Logo1 para resolución nativa (F4)
|
||||
SDL_Texture* logo2_base_texture_ = nullptr; // Logo2 para resolución base
|
||||
SDL_Texture* logo2_native_texture_ = nullptr; // Logo2 para resolución nativa (F4)
|
||||
SDL_Texture* logo1_base_texture_ = nullptr; // Logo1 para resolución base
|
||||
SDL_Texture* logo1_native_texture_ = nullptr; // Logo1 para resolución nativa (F4)
|
||||
SDL_Texture* logo2_base_texture_ = nullptr; // Logo2 para resolución base
|
||||
SDL_Texture* logo2_native_texture_ = nullptr; // Logo2 para resolución nativa (F4)
|
||||
|
||||
// Dimensiones pre-calculadas para cada textura
|
||||
int logo1_base_width_ = 0, logo1_base_height_ = 0;
|
||||
@@ -64,8 +64,8 @@ class AppLogo {
|
||||
int logo2_current_width_ = 0, logo2_current_height_ = 0;
|
||||
|
||||
// Resoluciones conocidas
|
||||
int base_screen_width_ = 0, base_screen_height_ = 0; // Resolución inicial
|
||||
int native_screen_width_ = 0, native_screen_height_ = 0; // Resolución nativa (F4)
|
||||
int base_screen_width_ = 0, base_screen_height_ = 0; // Resolución inicial
|
||||
int native_screen_width_ = 0, native_screen_height_ = 0; // Resolución nativa (F4)
|
||||
|
||||
// ====================================================================
|
||||
// Variables COMPARTIDAS (sincronización de ambos logos)
|
||||
@@ -74,8 +74,8 @@ class AppLogo {
|
||||
float timer_ = 0.0f; // Contador de tiempo para estado actual
|
||||
|
||||
// Alpha INDEPENDIENTE para cada logo (Logo 2 con retraso)
|
||||
int logo1_alpha_ = 0; // Alpha de Logo 1 (0-255)
|
||||
int logo2_alpha_ = 0; // Alpha de Logo 2 (0-255, con retraso)
|
||||
int logo1_alpha_ = 0; // Alpha de Logo 1 (0-255)
|
||||
int logo2_alpha_ = 0; // Alpha de Logo 2 (0-255, con retraso)
|
||||
|
||||
// Animación COMPARTIDA (misma para ambos logos, misma entrada y salida)
|
||||
AppLogoAnimationType current_animation_ = AppLogoAnimationType::ZOOM_ONLY;
|
||||
@@ -103,15 +103,15 @@ class AppLogo {
|
||||
SDL_Renderer* renderer_ = nullptr;
|
||||
|
||||
// Métodos privados auxiliares
|
||||
void updateLogoPosition(); // Centrar ambos logos en pantalla (superpuestos)
|
||||
void updateLogoPosition(); // Centrar ambos logos en pantalla (superpuestos)
|
||||
void renderWithGeometry(int logo_index); // Renderizar logo con vértices deformados (1 o 2)
|
||||
|
||||
// Funciones de easing
|
||||
float easeOutElastic(float t); // Elastic bounce out
|
||||
float easeOutBack(float t); // Overshoot out
|
||||
float easeOutBounce(float t); // Bounce easing (para BOUNCE_SQUASH)
|
||||
float easeInOutQuad(float t); // Quadratic easing (para ROTATE_SPIRAL)
|
||||
static float easeOutElastic(float t); // Elastic bounce out
|
||||
static float easeOutBack(float t); // Overshoot out
|
||||
static float easeOutBounce(float t); // Bounce easing (para BOUNCE_SQUASH)
|
||||
static float easeInOutQuad(float t); // Quadratic easing (para ROTATE_SPIRAL)
|
||||
|
||||
// Función auxiliar para elegir animación aleatoria
|
||||
AppLogoAnimationType getRandomAnimation();
|
||||
static AppLogoAnimationType getRandomAnimation();
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "help_overlay.hpp"
|
||||
|
||||
#include <algorithm> // for std::min
|
||||
#include <array> // for std::array
|
||||
|
||||
#include "defines.hpp"
|
||||
#include "text/textrenderer.hpp"
|
||||
#include "theme_manager.hpp"
|
||||
|
||||
@@ -18,68 +20,71 @@ HelpOverlay::HelpOverlay()
|
||||
box_y_(0),
|
||||
column1_width_(0),
|
||||
column2_width_(0),
|
||||
column3_width_(0),
|
||||
cached_texture_(nullptr),
|
||||
last_category_color_({0, 0, 0, 255}),
|
||||
last_content_color_({0, 0, 0, 255}),
|
||||
last_bg_color_({0, 0, 0, 255}),
|
||||
last_category_color_({.r = 0, .g = 0, .b = 0, .a = 255}),
|
||||
last_content_color_({.r = 0, .g = 0, .b = 0, .a = 255}),
|
||||
last_bg_color_({.r = 0, .g = 0, .b = 0, .a = 255}),
|
||||
texture_needs_rebuild_(true) {
|
||||
// Llenar lista de controles (organizados por categoría, equilibrado en 2 columnas)
|
||||
// Llenar lista de controles (organizados por categoría, equilibrado en 3 columnas)
|
||||
key_bindings_ = {
|
||||
// COLUMNA 1: SIMULACIÓN
|
||||
{"SIMULACIÓN", ""},
|
||||
{"1-8", "Escenarios (10 a 50,000 pelotas)"},
|
||||
{"F", "Toggle Física - Última Figura"},
|
||||
{"B", "Modo Boids (enjambre)"},
|
||||
{"ESPACIO", "Impulso contra gravedad"},
|
||||
{"G", "Toggle Gravedad ON/OFF"},
|
||||
{"CURSORES", "Dirección de gravedad"},
|
||||
{"", ""}, // Separador
|
||||
{.key = "SIMULACIÓN", .description = ""},
|
||||
{.key = "1-8", .description = "Escenarios (10 a 50.000 pelotas)"},
|
||||
{.key = "F", .description = "Cambia entre figura y física"},
|
||||
{.key = "B", .description = "Cambia entre boids y física"},
|
||||
{.key = "ESPACIO", .description = "Impulso contra la gravedad"},
|
||||
{.key = "G", .description = "Activar / Desactivar gravedad"},
|
||||
{.key = "CURSORES", .description = "Dirección de la gravedad"},
|
||||
{.key = "", .description = ""}, // Separador
|
||||
|
||||
// COLUMNA 1: FIGURAS 3D
|
||||
{"FIGURAS 3D", ""},
|
||||
{"Q/W/E/R", "Esfera/Lissajous/Hélice/Toroide"},
|
||||
{"T/Y/U/I", "Cubo/Cilindro/Icosaedro/Átomo"},
|
||||
{"O", "Forma PNG"},
|
||||
{"Num+/-", "Escalar figura"},
|
||||
{"Num*", "Reset escala"},
|
||||
{"Num/", "Toggle profundidad"},
|
||||
{"", ""}, // Separador
|
||||
|
||||
// COLUMNA 1: VISUAL
|
||||
{"VISUAL", ""},
|
||||
{"C", "Tema siguiente"},
|
||||
{"Shift+C", "Tema anterior"},
|
||||
{"NumEnter", "Página de temas"},
|
||||
{"N", "Cambiar sprite"},
|
||||
{"[new_col]", ""}, // Separador -> CAMBIO DE COLUMNA
|
||||
|
||||
// COLUMNA 2: PANTALLA
|
||||
{"PANTALLA", ""},
|
||||
{"F1/F2", "Zoom out/in (ventana)"},
|
||||
{"F3", "Fullscreen letterbox"},
|
||||
{"F4", "Fullscreen real"},
|
||||
{"F5", "Escalado (F3 activo)"},
|
||||
{"V", "Toggle V-Sync"},
|
||||
{"", ""}, // Separador
|
||||
{.key = "FIGURAS 3D", .description = ""},
|
||||
{.key = "Q/W/E/R", .description = "Esfera / Lissajous / Hélice / Toroide"},
|
||||
{.key = "T/Y/U/I", .description = "Cubo / Cilindro / Icosaedro / Átomo"},
|
||||
{.key = "Num+/-", .description = "Escalar figura"},
|
||||
{.key = "Num*", .description = "Reset escala"},
|
||||
{.key = "Num/", .description = "Activar / Desactivar profundidad"},
|
||||
{.key = "[new_col]", .description = ""}, // CAMBIO DE COLUMNA -> COLUMNA 2
|
||||
|
||||
// COLUMNA 2: MODOS
|
||||
{"MODOS", ""},
|
||||
{"D", "Modo DEMO"},
|
||||
{"Shift+D", "Pausar tema dinámico"},
|
||||
{"L", "Modo DEMO LITE"},
|
||||
{"K", "Modo LOGO (easter egg)"},
|
||||
{"", ""}, // Separador
|
||||
{.key = "MODOS", .description = ""},
|
||||
{.key = "D", .description = "Activar / Desactivar modo demo"},
|
||||
{.key = "L", .description = "Activar / Desactivar modo demo lite"},
|
||||
{.key = "K", .description = "Activar / Desactivar modo logo"},
|
||||
{.key = "", .description = ""}, // Separador
|
||||
|
||||
// COLUMNA 2: DEBUG/AYUDA
|
||||
{"DEBUG/AYUDA", ""},
|
||||
{"F12", "Toggle info debug"},
|
||||
{"H", "Esta ayuda"},
|
||||
{"ESC", "Salir"}};
|
||||
// COLUMNA 2: VISUAL
|
||||
{.key = "VISUAL", .description = ""},
|
||||
{.key = "C", .description = "Tema siguiente"},
|
||||
{.key = "Shift+C", .description = "Tema anterior"},
|
||||
{.key = "NumEnter", .description = "Página de temas"},
|
||||
{.key = "Shift+D", .description = "Pausar tema dinámico"},
|
||||
{.key = "N", .description = "Cambiar tamaño de pelota"},
|
||||
{.key = "X", .description = "Ciclar presets PostFX"},
|
||||
{.key = "[new_col]", .description = ""}, // CAMBIO DE COLUMNA -> COLUMNA 3
|
||||
|
||||
// COLUMNA 3: PANTALLA
|
||||
{.key = "PANTALLA", .description = ""},
|
||||
{.key = "F1", .description = "Disminuye ventana"},
|
||||
{.key = "F2", .description = "Aumenta ventana"},
|
||||
{.key = "F3", .description = "Pantalla completa"},
|
||||
{.key = "F4", .description = "Pantalla completa real"},
|
||||
{.key = "F5", .description = "Activar / Desactivar PostFX"},
|
||||
{.key = "F6", .description = "Cambia el escalado de pantalla"},
|
||||
{.key = "V", .description = "Activar / Desactivar V-Sync"},
|
||||
{.key = "", .description = ""}, // Separador
|
||||
|
||||
// COLUMNA 3: DEBUG/AYUDA
|
||||
{.key = "DEBUG / AYUDA", .description = ""},
|
||||
{.key = "F12", .description = "Activar / Desactivar info debug"},
|
||||
{.key = "H", .description = "Esta ayuda"},
|
||||
{.key = "ESC", .description = "Salir"}};
|
||||
}
|
||||
|
||||
HelpOverlay::~HelpOverlay() {
|
||||
// Destruir textura cacheada si existe
|
||||
if (cached_texture_) {
|
||||
if (cached_texture_ != nullptr) {
|
||||
SDL_DestroyTexture(cached_texture_);
|
||||
cached_texture_ = nullptr;
|
||||
}
|
||||
@@ -98,7 +103,7 @@ void HelpOverlay::initialize(SDL_Renderer* renderer, ThemeManager* theme_mgr, in
|
||||
|
||||
// Crear renderer de texto con tamaño dinámico
|
||||
text_renderer_ = new TextRenderer();
|
||||
text_renderer_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", font_size, true);
|
||||
text_renderer_->init(renderer, APP_FONT, font_size, true);
|
||||
|
||||
calculateBoxDimensions();
|
||||
}
|
||||
@@ -113,7 +118,9 @@ void HelpOverlay::updatePhysicalWindowSize(int physical_width, int physical_heig
|
||||
}
|
||||
|
||||
void HelpOverlay::reinitializeFontSize(int new_font_size) {
|
||||
if (!text_renderer_) return;
|
||||
if (text_renderer_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reinicializar text renderer con nuevo tamaño
|
||||
text_renderer_->reinitialize(new_font_size);
|
||||
@@ -132,7 +139,7 @@ void HelpOverlay::updateAll(int font_size, int physical_width, int physical_heig
|
||||
physical_height_ = physical_height;
|
||||
|
||||
// Reinicializar text renderer con nuevo tamaño (si cambió)
|
||||
if (text_renderer_) {
|
||||
if (text_renderer_ != nullptr) {
|
||||
text_renderer_->reinitialize(font_size);
|
||||
}
|
||||
|
||||
@@ -144,7 +151,7 @@ void HelpOverlay::updateAll(int font_size, int physical_width, int physical_heig
|
||||
}
|
||||
|
||||
void HelpOverlay::calculateTextDimensions(int& max_width, int& total_height) {
|
||||
if (!text_renderer_) {
|
||||
if (text_renderer_ == nullptr) {
|
||||
max_width = 0;
|
||||
total_height = 0;
|
||||
return;
|
||||
@@ -157,12 +164,13 @@ void HelpOverlay::calculateTextDimensions(int& max_width, int& total_height) {
|
||||
// Calcular ancho máximo por columna
|
||||
int max_col1_width = 0;
|
||||
int max_col2_width = 0;
|
||||
int max_col3_width = 0;
|
||||
int current_column = 0;
|
||||
|
||||
for (const auto& binding : key_bindings_) {
|
||||
// Cambio de columna
|
||||
if (strcmp(binding.key, "[new_col]") == 0) {
|
||||
current_column = 1;
|
||||
current_column++;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -186,53 +194,57 @@ void HelpOverlay::calculateTextDimensions(int& max_width, int& total_height) {
|
||||
// Actualizar máximo de columna correspondiente
|
||||
if (current_column == 0) {
|
||||
max_col1_width = std::max(max_col1_width, line_width);
|
||||
} else {
|
||||
} else if (current_column == 1) {
|
||||
max_col2_width = std::max(max_col2_width, line_width);
|
||||
} else {
|
||||
max_col3_width = std::max(max_col3_width, line_width);
|
||||
}
|
||||
}
|
||||
|
||||
// Almacenar anchos de columnas en miembros para uso posterior
|
||||
column1_width_ = max_col1_width;
|
||||
column2_width_ = max_col2_width;
|
||||
column3_width_ = max_col3_width;
|
||||
|
||||
// Ancho total: 2 columnas + 3 paddings (izq, medio, der)
|
||||
max_width = max_col1_width + max_col2_width + padding * 3;
|
||||
// Gap entre columnas: doble del padding para dar más respiro
|
||||
int col_gap = padding * 2;
|
||||
|
||||
// Altura: contar líneas REALES en cada columna
|
||||
int col1_lines = 0;
|
||||
int col2_lines = 0;
|
||||
// Ancho total: 3 columnas + padding izq/der + 2 gaps entre columnas
|
||||
max_width = max_col1_width + max_col2_width + max_col3_width + padding * 2 + col_gap * 2;
|
||||
|
||||
// Calcular altura real simulando exactamente lo que hace el render
|
||||
std::array<int, 3> col_heights = {0, 0, 0};
|
||||
current_column = 0;
|
||||
|
||||
for (const auto& binding : key_bindings_) {
|
||||
// Cambio de columna
|
||||
if (strcmp(binding.key, "[new_col]") == 0) {
|
||||
current_column = 1;
|
||||
current_column++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Separador vacío no cuenta como línea
|
||||
if (binding.key[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Contar línea (ya sea encabezado o contenido)
|
||||
if (current_column == 0) {
|
||||
col1_lines++;
|
||||
col_heights[current_column] += line_height; // separador vacío
|
||||
} else if (binding.description[0] == '\0') {
|
||||
col_heights[current_column] += line_height; // encabezado
|
||||
} else {
|
||||
col2_lines++;
|
||||
col_heights[current_column] += line_height; // línea normal
|
||||
}
|
||||
}
|
||||
|
||||
// Usar la columna más larga para calcular altura
|
||||
int max_column_lines = std::max(col1_lines, col2_lines);
|
||||
int content_height = std::max({col_heights[0], col_heights[1], col_heights[2]});
|
||||
|
||||
// Altura: título (2 líneas) + contenido + padding superior e inferior
|
||||
total_height = line_height * 2 + max_column_lines * line_height + padding * 2;
|
||||
// Eliminar el line_gap de la última línea: ese gap es espacio entre líneas,
|
||||
// pero la última línea no tiene siguiente, así que queda como padding muerto.
|
||||
int glyph_height = text_renderer_->getGlyphHeight();
|
||||
int visual_content_height = content_height - (line_height - glyph_height);
|
||||
|
||||
total_height = visual_content_height + padding * 2;
|
||||
}
|
||||
|
||||
void HelpOverlay::calculateBoxDimensions() {
|
||||
// Calcular dimensiones necesarias según el texto
|
||||
int text_width, text_height;
|
||||
int text_width;
|
||||
int text_height;
|
||||
calculateTextDimensions(text_width, text_height);
|
||||
|
||||
// Aplicar límites máximos: 95% ancho, 90% altura
|
||||
@@ -248,22 +260,24 @@ void HelpOverlay::calculateBoxDimensions() {
|
||||
}
|
||||
|
||||
void HelpOverlay::rebuildCachedTexture() {
|
||||
if (!renderer_ || !theme_mgr_ || !text_renderer_) return;
|
||||
if ((renderer_ == nullptr) || (theme_mgr_ == nullptr) || (text_renderer_ == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Destruir textura anterior si existe
|
||||
if (cached_texture_) {
|
||||
if (cached_texture_ != nullptr) {
|
||||
SDL_DestroyTexture(cached_texture_);
|
||||
cached_texture_ = nullptr;
|
||||
}
|
||||
|
||||
// Crear nueva textura del tamaño del overlay
|
||||
cached_texture_ = SDL_CreateTexture(renderer_,
|
||||
SDL_PIXELFORMAT_RGBA8888,
|
||||
SDL_TEXTUREACCESS_TARGET,
|
||||
box_width_,
|
||||
box_height_);
|
||||
SDL_PIXELFORMAT_RGBA8888,
|
||||
SDL_TEXTUREACCESS_TARGET,
|
||||
box_width_,
|
||||
box_height_);
|
||||
|
||||
if (!cached_texture_) {
|
||||
if (cached_texture_ == nullptr) {
|
||||
SDL_Log("Error al crear textura cacheada: %s", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
@@ -285,42 +299,46 @@ void HelpOverlay::rebuildCachedTexture() {
|
||||
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
|
||||
|
||||
// Obtener colores actuales del tema
|
||||
int notif_bg_r, notif_bg_g, notif_bg_b;
|
||||
int notif_bg_r;
|
||||
int notif_bg_g;
|
||||
int notif_bg_b;
|
||||
theme_mgr_->getCurrentNotificationBackgroundColor(notif_bg_r, notif_bg_g, notif_bg_b);
|
||||
|
||||
// Renderizar fondo del overlay a la textura
|
||||
float alpha = 0.85f;
|
||||
SDL_Vertex bg_vertices[4];
|
||||
std::array<SDL_Vertex, 4> bg_vertices{};
|
||||
|
||||
float r = notif_bg_r / 255.0f;
|
||||
float g = notif_bg_g / 255.0f;
|
||||
float b = notif_bg_b / 255.0f;
|
||||
|
||||
// Vértices del fondo (posición relativa 0,0 porque estamos renderizando a textura)
|
||||
bg_vertices[0].position = {0, 0};
|
||||
bg_vertices[0].tex_coord = {0.0f, 0.0f};
|
||||
bg_vertices[0].color = {r, g, b, alpha};
|
||||
bg_vertices[0].position = {.x = 0, .y = 0};
|
||||
bg_vertices[0].tex_coord = {.x = 0.0f, .y = 0.0f};
|
||||
bg_vertices[0].color = {.r = r, .g = g, .b = b, .a = alpha};
|
||||
|
||||
bg_vertices[1].position = {static_cast<float>(box_width_), 0};
|
||||
bg_vertices[1].tex_coord = {1.0f, 0.0f};
|
||||
bg_vertices[1].color = {r, g, b, alpha};
|
||||
bg_vertices[1].position = {.x = static_cast<float>(box_width_), .y = 0};
|
||||
bg_vertices[1].tex_coord = {.x = 1.0f, .y = 0.0f};
|
||||
bg_vertices[1].color = {.r = r, .g = g, .b = b, .a = alpha};
|
||||
|
||||
bg_vertices[2].position = {static_cast<float>(box_width_), static_cast<float>(box_height_)};
|
||||
bg_vertices[2].tex_coord = {1.0f, 1.0f};
|
||||
bg_vertices[2].color = {r, g, b, alpha};
|
||||
bg_vertices[2].position = {.x = static_cast<float>(box_width_), .y = static_cast<float>(box_height_)};
|
||||
bg_vertices[2].tex_coord = {.x = 1.0f, .y = 1.0f};
|
||||
bg_vertices[2].color = {.r = r, .g = g, .b = b, .a = alpha};
|
||||
|
||||
bg_vertices[3].position = {0, static_cast<float>(box_height_)};
|
||||
bg_vertices[3].tex_coord = {0.0f, 1.0f};
|
||||
bg_vertices[3].color = {r, g, b, alpha};
|
||||
bg_vertices[3].position = {.x = 0, .y = static_cast<float>(box_height_)};
|
||||
bg_vertices[3].tex_coord = {.x = 0.0f, .y = 1.0f};
|
||||
bg_vertices[3].color = {.r = r, .g = g, .b = b, .a = alpha};
|
||||
|
||||
int bg_indices[6] = {0, 1, 2, 2, 3, 0};
|
||||
SDL_RenderGeometry(renderer_, nullptr, bg_vertices, 4, bg_indices, 6);
|
||||
std::array<int, 6> bg_indices = {0, 1, 2, 2, 3, 0};
|
||||
SDL_RenderGeometry(renderer_, nullptr, bg_vertices.data(), 4, bg_indices.data(), 6);
|
||||
|
||||
// Renderizar texto del overlay (ajustando coordenadas para que sean relativas a 0,0)
|
||||
// Necesito renderizar el texto igual que en renderHelpText() pero con coordenadas ajustadas
|
||||
|
||||
// Obtener colores para el texto
|
||||
int text_r, text_g, text_b;
|
||||
int text_r;
|
||||
int text_g;
|
||||
int text_b;
|
||||
theme_mgr_->getCurrentThemeTextColor(text_r, text_g, text_b);
|
||||
SDL_Color category_color = {static_cast<Uint8>(text_r), static_cast<Uint8>(text_g), static_cast<Uint8>(text_b), 255};
|
||||
|
||||
@@ -330,60 +348,68 @@ void HelpOverlay::rebuildCachedTexture() {
|
||||
// Guardar colores actuales para comparación futura
|
||||
last_category_color_ = category_color;
|
||||
last_content_color_ = content_color;
|
||||
last_bg_color_ = {static_cast<Uint8>(notif_bg_r), static_cast<Uint8>(notif_bg_g), static_cast<Uint8>(notif_bg_b), 255};
|
||||
last_bg_color_ = {.r = static_cast<Uint8>(notif_bg_r), .g = static_cast<Uint8>(notif_bg_g), .b = static_cast<Uint8>(notif_bg_b), .a = 255};
|
||||
|
||||
// Configuración de espaciado
|
||||
int line_height = text_renderer_->getTextHeight();
|
||||
// Padding dinámico basado en altura física: 25px para >= 600px, escalado proporcionalmente para menores
|
||||
int padding = (physical_height_ >= 600) ? 25 : std::max(10, physical_height_ / 24);
|
||||
int col_gap = padding * 2;
|
||||
|
||||
int current_x = padding; // Coordenadas relativas a la textura (0,0)
|
||||
// Posición X de inicio de cada columna
|
||||
std::array<int, 3> col_start{};
|
||||
col_start[0] = padding;
|
||||
col_start[1] = padding + column1_width_ + col_gap;
|
||||
col_start[2] = padding + column1_width_ + col_gap + column2_width_ + col_gap;
|
||||
|
||||
// Ancho de cada columna (para centrado interno)
|
||||
std::array<int, 3> col_width = {column1_width_, column2_width_, column3_width_};
|
||||
|
||||
int glyph_height = text_renderer_->getGlyphHeight();
|
||||
int current_y = padding;
|
||||
int current_column = 0;
|
||||
|
||||
// Título principal
|
||||
const char* title = "CONTROLES - ViBe3 Physics";
|
||||
int title_width = text_renderer_->getTextWidthPhysical(title);
|
||||
text_renderer_->printAbsolute(box_width_ / 2 - title_width / 2, current_y, title, category_color);
|
||||
current_y += line_height * 2;
|
||||
|
||||
int content_start_y = current_y;
|
||||
|
||||
// Renderizar cada línea
|
||||
for (const auto& binding : key_bindings_) {
|
||||
if (strcmp(binding.key, "[new_col]") == 0 && binding.description[0] == '\0') {
|
||||
if (current_column == 0) {
|
||||
current_column = 1;
|
||||
current_x = padding + column1_width_ + padding; // Usar ancho real de columna 1
|
||||
if (current_column < 2) {
|
||||
current_column++;
|
||||
current_y = content_start_y;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// CHECK PADDING INFERIOR ANTES de escribir la línea (AMBAS COLUMNAS)
|
||||
// Verificar si la PRÓXIMA línea cabrá dentro del box con padding inferior
|
||||
if (current_y + line_height >= box_height_ - padding) {
|
||||
if (current_column == 0) {
|
||||
// Columna 0 llena: cambiar a columna 1
|
||||
current_column = 1;
|
||||
current_x = padding + column1_width_ + padding;
|
||||
current_y = content_start_y;
|
||||
} else {
|
||||
// Columna 1 llena: omitir resto de texto (no cabe)
|
||||
// Preferible omitir que sobresalir del overlay
|
||||
continue;
|
||||
}
|
||||
// CHECK PADDING INFERIOR ANTES de escribir la línea
|
||||
// Usamos glyph_height (no line_height) porque el gap después de la última línea no ocupa espacio visual
|
||||
if (current_y + glyph_height > box_height_ - padding) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int cx = col_start[current_column];
|
||||
int cw = col_width[current_column];
|
||||
|
||||
if (binding.description[0] == '\0') {
|
||||
text_renderer_->printAbsolute(current_x, current_y, binding.key, category_color);
|
||||
current_y += line_height + 2;
|
||||
if (binding.key[0] == '\0') {
|
||||
// Separador vacío: avanzar una línea completa
|
||||
current_y += line_height;
|
||||
} else {
|
||||
// Encabezado de sección — centrado en la columna
|
||||
int w = text_renderer_->getTextWidthPhysical(binding.key);
|
||||
text_renderer_->printAbsolute(cx + ((cw - w) / 2), current_y, binding.key, category_color);
|
||||
current_y += line_height;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
text_renderer_->printAbsolute(current_x, current_y, binding.key, content_color);
|
||||
// Par tecla+descripción — centrado como bloque en la columna
|
||||
int key_width = text_renderer_->getTextWidthPhysical(binding.key);
|
||||
text_renderer_->printAbsolute(current_x + key_width + 10, current_y, binding.description, content_color);
|
||||
int desc_width = text_renderer_->getTextWidthPhysical(binding.description);
|
||||
int total_width = key_width + 10 + desc_width;
|
||||
int line_x = cx + ((cw - total_width) / 2);
|
||||
|
||||
text_renderer_->printAbsolute(line_x, current_y, binding.key, category_color);
|
||||
text_renderer_->printAbsolute(line_x + key_width + 10, current_y, binding.description, content_color);
|
||||
|
||||
current_y += line_height;
|
||||
}
|
||||
@@ -396,13 +422,19 @@ void HelpOverlay::rebuildCachedTexture() {
|
||||
}
|
||||
|
||||
void HelpOverlay::render(SDL_Renderer* renderer) {
|
||||
if (!visible_) return;
|
||||
if (!visible_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtener colores actuales del tema
|
||||
int notif_bg_r, notif_bg_g, notif_bg_b;
|
||||
int notif_bg_r;
|
||||
int notif_bg_g;
|
||||
int notif_bg_b;
|
||||
theme_mgr_->getCurrentNotificationBackgroundColor(notif_bg_r, notif_bg_g, notif_bg_b);
|
||||
|
||||
int text_r, text_g, text_b;
|
||||
int text_r;
|
||||
int text_g;
|
||||
int text_b;
|
||||
theme_mgr_->getCurrentThemeTextColor(text_r, text_g, text_b);
|
||||
|
||||
Color ball_color = theme_mgr_->getInterpolatedColor(0);
|
||||
@@ -416,22 +448,24 @@ void HelpOverlay::render(SDL_Renderer* renderer) {
|
||||
constexpr int COLOR_CHANGE_THRESHOLD = 5;
|
||||
bool colors_changed =
|
||||
(abs(current_bg.r - last_bg_color_.r) > COLOR_CHANGE_THRESHOLD ||
|
||||
abs(current_bg.g - last_bg_color_.g) > COLOR_CHANGE_THRESHOLD ||
|
||||
abs(current_bg.b - last_bg_color_.b) > COLOR_CHANGE_THRESHOLD ||
|
||||
abs(current_category.r - last_category_color_.r) > COLOR_CHANGE_THRESHOLD ||
|
||||
abs(current_category.g - last_category_color_.g) > COLOR_CHANGE_THRESHOLD ||
|
||||
abs(current_category.b - last_category_color_.b) > COLOR_CHANGE_THRESHOLD ||
|
||||
abs(current_content.r - last_content_color_.r) > COLOR_CHANGE_THRESHOLD ||
|
||||
abs(current_content.g - last_content_color_.g) > COLOR_CHANGE_THRESHOLD ||
|
||||
abs(current_content.b - last_content_color_.b) > COLOR_CHANGE_THRESHOLD);
|
||||
abs(current_bg.g - last_bg_color_.g) > COLOR_CHANGE_THRESHOLD ||
|
||||
abs(current_bg.b - last_bg_color_.b) > COLOR_CHANGE_THRESHOLD ||
|
||||
abs(current_category.r - last_category_color_.r) > COLOR_CHANGE_THRESHOLD ||
|
||||
abs(current_category.g - last_category_color_.g) > COLOR_CHANGE_THRESHOLD ||
|
||||
abs(current_category.b - last_category_color_.b) > COLOR_CHANGE_THRESHOLD ||
|
||||
abs(current_content.r - last_content_color_.r) > COLOR_CHANGE_THRESHOLD ||
|
||||
abs(current_content.g - last_content_color_.g) > COLOR_CHANGE_THRESHOLD ||
|
||||
abs(current_content.b - last_content_color_.b) > COLOR_CHANGE_THRESHOLD);
|
||||
|
||||
// Regenerar textura si es necesario (colores cambiaron O flag de rebuild activo)
|
||||
if (texture_needs_rebuild_ || colors_changed || !cached_texture_) {
|
||||
if (texture_needs_rebuild_ || colors_changed || (cached_texture_ == nullptr)) {
|
||||
rebuildCachedTexture();
|
||||
}
|
||||
|
||||
// Si no hay textura cacheada (error), salir
|
||||
if (!cached_texture_) return;
|
||||
if (cached_texture_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// CRÍTICO: Habilitar alpha blending para que la transparencia funcione
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
@@ -443,8 +477,8 @@ void HelpOverlay::render(SDL_Renderer* renderer) {
|
||||
// Calcular posición centrada dentro del VIEWPORT, no de la pantalla física
|
||||
// viewport.w y viewport.h son las dimensiones del área visible
|
||||
// viewport.x y viewport.y son el offset de las barras negras
|
||||
int centered_x = viewport.x + (viewport.w - box_width_) / 2;
|
||||
int centered_y = viewport.y + (viewport.h - box_height_) / 2;
|
||||
int centered_x = viewport.x + ((viewport.w - box_width_) / 2);
|
||||
int centered_y = viewport.y + ((viewport.h - box_height_) / 2);
|
||||
|
||||
// Renderizar la textura cacheada centrada en el viewport
|
||||
SDL_FRect dest_rect;
|
||||
|
||||
@@ -17,88 +17,89 @@ class TextRenderer;
|
||||
* Toggle on/off con tecla H. La simulación continúa en el fondo.
|
||||
*/
|
||||
class HelpOverlay {
|
||||
public:
|
||||
HelpOverlay();
|
||||
~HelpOverlay();
|
||||
public:
|
||||
HelpOverlay();
|
||||
~HelpOverlay();
|
||||
|
||||
/**
|
||||
* @brief Inicializa el overlay con renderer y theme manager
|
||||
*/
|
||||
void initialize(SDL_Renderer* renderer, ThemeManager* theme_mgr, int physical_width, int physical_height, int font_size);
|
||||
/**
|
||||
* @brief Inicializa el overlay con renderer y theme manager
|
||||
*/
|
||||
void initialize(SDL_Renderer* renderer, ThemeManager* theme_mgr, int physical_width, int physical_height, int font_size);
|
||||
|
||||
/**
|
||||
* @brief Renderiza el overlay si está visible
|
||||
*/
|
||||
void render(SDL_Renderer* renderer);
|
||||
/**
|
||||
* @brief Renderiza el overlay si está visible
|
||||
*/
|
||||
void render(SDL_Renderer* renderer);
|
||||
|
||||
/**
|
||||
* @brief Actualiza dimensiones físicas de ventana (zoom, fullscreen, etc.)
|
||||
*/
|
||||
void updatePhysicalWindowSize(int physical_width, int physical_height);
|
||||
/**
|
||||
* @brief Actualiza dimensiones físicas de ventana (zoom, fullscreen, etc.)
|
||||
*/
|
||||
void updatePhysicalWindowSize(int physical_width, int physical_height);
|
||||
|
||||
/**
|
||||
* @brief Reinitializa el tamaño de fuente (cuando cambia el tamaño de ventana)
|
||||
*/
|
||||
void reinitializeFontSize(int new_font_size);
|
||||
/**
|
||||
* @brief Reinitializa el tamaño de fuente (cuando cambia el tamaño de ventana)
|
||||
*/
|
||||
void reinitializeFontSize(int new_font_size);
|
||||
|
||||
/**
|
||||
* @brief Actualiza font size Y dimensiones físicas de forma atómica
|
||||
* @param font_size Tamaño de fuente actual
|
||||
* @param physical_width Nueva anchura física
|
||||
* @param physical_height Nueva altura física
|
||||
*/
|
||||
void updateAll(int font_size, int physical_width, int physical_height);
|
||||
/**
|
||||
* @brief Actualiza font size Y dimensiones físicas de forma atómica
|
||||
* @param font_size Tamaño de fuente actual
|
||||
* @param physical_width Nueva anchura física
|
||||
* @param physical_height Nueva altura física
|
||||
*/
|
||||
void updateAll(int font_size, int physical_width, int physical_height);
|
||||
|
||||
/**
|
||||
* @brief Toggle visibilidad del overlay
|
||||
*/
|
||||
void toggle();
|
||||
/**
|
||||
* @brief Toggle visibilidad del overlay
|
||||
*/
|
||||
void toggle();
|
||||
|
||||
/**
|
||||
* @brief Consulta si el overlay está visible
|
||||
*/
|
||||
bool isVisible() const { return visible_; }
|
||||
/**
|
||||
* @brief Consulta si el overlay está visible
|
||||
*/
|
||||
bool isVisible() const { return visible_; }
|
||||
|
||||
private:
|
||||
SDL_Renderer* renderer_;
|
||||
ThemeManager* theme_mgr_;
|
||||
TextRenderer* text_renderer_; // Renderer de texto para la ayuda
|
||||
int physical_width_;
|
||||
int physical_height_;
|
||||
bool visible_;
|
||||
private:
|
||||
SDL_Renderer* renderer_;
|
||||
ThemeManager* theme_mgr_;
|
||||
TextRenderer* text_renderer_; // Renderer de texto para la ayuda
|
||||
int physical_width_;
|
||||
int physical_height_;
|
||||
bool visible_;
|
||||
|
||||
// Dimensiones calculadas del recuadro (anchura dinámica según texto, centrado)
|
||||
int box_width_;
|
||||
int box_height_;
|
||||
int box_x_;
|
||||
int box_y_;
|
||||
// Dimensiones calculadas del recuadro (anchura dinámica según texto, centrado)
|
||||
int box_width_;
|
||||
int box_height_;
|
||||
int box_x_;
|
||||
int box_y_;
|
||||
|
||||
// Anchos individuales de cada columna (para evitar solapamiento)
|
||||
int column1_width_;
|
||||
int column2_width_;
|
||||
// Anchos individuales de cada columna (para evitar solapamiento)
|
||||
int column1_width_;
|
||||
int column2_width_;
|
||||
int column3_width_;
|
||||
|
||||
// Sistema de caché para optimización de rendimiento
|
||||
SDL_Texture* cached_texture_; // Textura cacheada del overlay completo
|
||||
SDL_Color last_category_color_; // Último color de categorías renderizado
|
||||
SDL_Color last_content_color_; // Último color de contenido renderizado
|
||||
SDL_Color last_bg_color_; // Último color de fondo renderizado
|
||||
bool texture_needs_rebuild_; // Flag para forzar regeneración de textura
|
||||
// Sistema de caché para optimización de rendimiento
|
||||
SDL_Texture* cached_texture_; // Textura cacheada del overlay completo
|
||||
SDL_Color last_category_color_; // Último color de categorías renderizado
|
||||
SDL_Color last_content_color_; // Último color de contenido renderizado
|
||||
SDL_Color last_bg_color_; // Último color de fondo renderizado
|
||||
bool texture_needs_rebuild_; // Flag para forzar regeneración de textura
|
||||
|
||||
// Calcular dimensiones del texto más largo
|
||||
void calculateTextDimensions(int& max_width, int& total_height);
|
||||
// Calcular dimensiones del texto más largo
|
||||
void calculateTextDimensions(int& max_width, int& total_height);
|
||||
|
||||
// Calcular dimensiones del recuadro según tamaño de ventana y texto
|
||||
void calculateBoxDimensions();
|
||||
// Calcular dimensiones del recuadro según tamaño de ventana y texto
|
||||
void calculateBoxDimensions();
|
||||
|
||||
// Regenerar textura cacheada del overlay
|
||||
void rebuildCachedTexture();
|
||||
// Regenerar textura cacheada del overlay
|
||||
void rebuildCachedTexture();
|
||||
|
||||
// Estructura para par tecla-descripción
|
||||
struct KeyBinding {
|
||||
const char* key;
|
||||
const char* description;
|
||||
};
|
||||
// Estructura para par tecla-descripción
|
||||
struct KeyBinding {
|
||||
const char* key;
|
||||
const char* description;
|
||||
};
|
||||
|
||||
// Lista de todos los controles (se llena en constructor)
|
||||
std::vector<KeyBinding> key_bindings_;
|
||||
// Lista de todos los controles (se llena en constructor)
|
||||
std::vector<KeyBinding> key_bindings_;
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user