Files
vibe3_physics/source/shapes/png_shape.cpp
Sergio Valor 4f900eaa57 Implementar pivoteo sutil en PNG_SHAPE y eliminar debug output
Cambios:

1. **PNG_SHAPE pivoteo sutil** (similar a WAVE_GRID):
   - Añadidas variables tilt_x_ y tilt_y_ en png_shape.h
   - Actualización continua de tilt en update()
   - Aplicación de pivoteo en getPoint3D() con:
     * Cálculo correcto de logo_size para normalización
     * Normalización a rango [-1, 1] usando logo_size * 0.5
     * Amplitudes 0.15 y 0.1 (matching WAVE_GRID)
     * z_tilt proporcional al tamaño del logo
   - Fix crítico: usar z_base en lugar de z fijo (línea 390)

2. **Eliminación de debug output**:
   - Removidos 13 std::cout de png_shape.cpp
   - Removidos 2 std::cout de engine.cpp (Logo Mode)
   - Consola ahora limpia sin mensajes [PNG_SHAPE]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 23:59:45 +02:00

386 lines
14 KiB
C++

#include "png_shape.h"
#include "../defines.h"
#include "../external/stb_image.h"
#include <cmath>
#include <algorithm>
#include <iostream>
#include <map>
PNGShape::PNGShape(const char* png_path) {
// Cargar PNG desde path
if (!loadPNG(png_path)) {
// Fallback: generar un cuadrado simple si falla la carga
image_width_ = 10;
image_height_ = 10;
pixel_data_.resize(100, true); // Cuadrado 10x10 blanco
}
}
bool PNGShape::loadPNG(const char* path) {
int width, height, channels;
unsigned char* data = stbi_load(path, &width, &height, &channels, 1); // Forzar 1 canal (grayscale)
if (!data) {
return false;
}
image_width_ = width;
image_height_ = height;
pixel_data_.resize(width * height);
// Convertir a mapa booleano (true = píxel blanco/visible, false = negro/transparente)
for (int i = 0; i < width * height; i++) {
pixel_data_[i] = (data[i] > 128); // Umbral: >128 = blanco
}
stbi_image_free(data);
return true;
}
void PNGShape::detectEdges() {
edge_points_.clear();
// Detectar píxeles del contorno (píxeles blancos con al menos un vecino negro)
for (int y = 0; y < image_height_; y++) {
for (int x = 0; x < image_width_; x++) {
int idx = y * image_width_ + x;
if (!pixel_data_[idx]) continue; // Solo píxeles blancos
// Verificar vecinos (arriba, abajo, izq, der)
bool is_edge = false;
if (x == 0 || x == image_width_ - 1 || y == 0 || y == image_height_ - 1) {
is_edge = true; // Bordes de la imagen
} else {
// Verificar 4 vecinos
if (!pixel_data_[idx - 1] || // Izquierda
!pixel_data_[idx + 1] || // Derecha
!pixel_data_[idx - image_width_] || // Arriba
!pixel_data_[idx + image_width_]) { // Abajo
is_edge = true;
}
}
if (is_edge) {
edge_points_.push_back({static_cast<float>(x), static_cast<float>(y)});
}
}
}
}
void PNGShape::floodFill() {
// TODO: Implementar flood-fill para Enfoque B (voxelización)
// Por ahora, rellenar con todos los píxeles blancos
filled_points_.clear();
for (int y = 0; y < image_height_; y++) {
for (int x = 0; x < image_width_; x++) {
int idx = y * image_width_ + x;
if (pixel_data_[idx]) {
filled_points_.push_back({static_cast<float>(x), static_cast<float>(y)});
}
}
}
}
void PNGShape::generateExtrudedPoints() {
if (PNG_USE_EDGES_ONLY) {
// Usar solo bordes (contorno) de las letras
detectEdges();
} else {
// Usar relleno completo (todos los píxeles blancos)
floodFill();
}
}
void PNGShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points;
extrusion_depth_ = screen_height * PNG_EXTRUSION_DEPTH_FACTOR;
num_layers_ = PNG_NUM_EXTRUSION_LAYERS;
// Generar AMBOS conjuntos de puntos (relleno Y bordes)
floodFill(); // Generar filled_points_
detectEdges(); // Generar edge_points_
// Guardar copias originales (las funciones de filtrado modifican los vectores)
std::vector<Point2D> filled_points_original = filled_points_;
std::vector<Point2D> edge_points_original = edge_points_;
// Conjunto de puntos ACTIVO (será modificado por filtros)
std::vector<Point2D> active_points_data;
std::string mode_name = "";
// === SISTEMA DE DISTRIBUCIÓN ADAPTATIVA ===
// Estrategia: Optimizar según número de pelotas disponibles
// Objetivo: SIEMPRE intentar usar relleno primero, solo bordes si es necesario
size_t num_2d_points = 0;
size_t total_3d_points = 0;
// NIVEL 1: Decidir punto de partida (relleno o bordes por configuración)
if (PNG_USE_EDGES_ONLY) {
active_points_data = edge_points_original;
mode_name = "BORDES (config)";
} else {
active_points_data = filled_points_original;
mode_name = "RELLENO";
}
num_2d_points = active_points_data.size();
total_3d_points = num_2d_points * num_layers_;
// NIVEL 2: Reducir capas AGRESIVAMENTE hasta 1 (priorizar calidad 2D sobre profundidad 3D)
// Objetivo: Llenar bien el texto en 2D antes de reducir píxeles
while (num_layers_ > 1 && num_points < static_cast<int>(total_3d_points)) {
num_layers_ = std::max(1, num_layers_ / 2);
total_3d_points = num_2d_points * num_layers_;
}
// NIVEL 3: Filas alternas en RELLENO (solo si 1 capa no alcanza)
// Esto permite usar relleno incluso con pocas pelotas
int row_skip = 1;
if (!PNG_USE_EDGES_ONLY) { // Solo si empezamos con relleno
while (row_skip < 5 && num_points < static_cast<int>(total_3d_points)) {
row_skip++;
// ✅ CLAVE: Recalcular desde el ORIGINAL cada vez (no desde el filtrado previo)
active_points_data = extractAlternateRows(filled_points_original, row_skip);
num_2d_points = active_points_data.size();
total_3d_points = num_2d_points * num_layers_;
mode_name = "RELLENO + FILAS/" + std::to_string(row_skip);
}
}
// NIVEL 4: Cambiar a BORDES (solo si relleno con filas alternas no alcanza)
if (!PNG_USE_EDGES_ONLY && num_points < static_cast<int>(total_3d_points)) {
active_points_data = edge_points_original;
mode_name = "BORDES (auto)";
num_2d_points = active_points_data.size();
total_3d_points = num_2d_points * num_layers_;
row_skip = 1; // Reset row_skip para bordes
}
// NIVEL 5: Filas alternas en BORDES (si aún no alcanza)
while (row_skip < 8 && num_points < static_cast<int>(total_3d_points)) {
row_skip++;
// ✅ CLAVE: Recalcular desde edge_points_original cada vez
active_points_data = extractAlternateRows(edge_points_original, row_skip);
num_2d_points = active_points_data.size();
total_3d_points = num_2d_points * num_layers_;
if (mode_name.find("FILAS") == std::string::npos) {
mode_name += " + FILAS/" + std::to_string(row_skip);
}
}
// NIVEL 6: Vértices/esquinas (último recurso, muy pocas pelotas)
if (num_points < static_cast<int>(total_3d_points) && num_points < 150) {
// Determinar desde qué conjunto extraer vértices (el que esté activo actualmente)
const std::vector<Point2D>& source_for_vertices = (mode_name.find("BORDES") != std::string::npos)
? edge_points_original
: filled_points_original;
std::vector<Point2D> vertices = extractCornerVertices(source_for_vertices);
if (!vertices.empty() && vertices.size() < active_points_data.size()) {
active_points_data = vertices;
num_2d_points = active_points_data.size();
total_3d_points = num_2d_points * num_layers_;
mode_name = "VÉRTICES";
}
}
// ✅ CLAVE: Guardar el conjunto de puntos optimizado final en optimized_points_ (usado por getPoint3D)
optimized_points_ = active_points_data;
// Calcular escala para centrar la imagen en pantalla
float max_dimension = std::max(static_cast<float>(image_width_), static_cast<float>(image_height_));
scale_factor_ = (screen_height * PNG_SIZE_FACTOR) / max_dimension;
// Calcular offset para centrar
center_offset_x_ = image_width_ * 0.5f;
center_offset_y_ = image_height_ * 0.5f;
}
// Extraer filas alternas de puntos (FUNCIÓN PURA: no modifica parámetros)
// Recibe vector original y devuelve nuevo vector filtrado
std::vector<PNGShape::Point2D> PNGShape::extractAlternateRows(const std::vector<Point2D>& source, int row_skip) {
std::vector<Point2D> result;
if (row_skip <= 1 || source.empty()) {
return source; // Sin filtrado, devolver copia del original
}
// Organizar puntos por fila (Y)
std::map<int, std::vector<Point2D>> rows;
for (const auto& p : source) {
int row = static_cast<int>(p.y);
rows[row].push_back(p);
}
// Tomar solo cada N filas
int row_counter = 0;
for (const auto& [row_y, row_points] : rows) {
if (row_counter % row_skip == 0) {
result.insert(result.end(), row_points.begin(), row_points.end());
}
row_counter++;
}
return result;
}
// Extraer vértices y esquinas (FUNCIÓN PURA: devuelve nuevo vector)
std::vector<PNGShape::Point2D> PNGShape::extractCornerVertices(const std::vector<Point2D>& source) {
std::vector<Point2D> result;
if (source.empty()) {
return result;
}
// Estrategia simple: tomar bordes extremos de cada fila
// Esto da el "esqueleto" mínimo de las letras
std::map<int, std::pair<float, float>> row_extremes; // Y -> (min_x, max_x)
for (const auto& p : source) {
int row = static_cast<int>(p.y);
if (row_extremes.find(row) == row_extremes.end()) {
row_extremes[row] = {p.x, p.x};
} else {
row_extremes[row].first = std::min(row_extremes[row].first, p.x);
row_extremes[row].second = std::max(row_extremes[row].second, p.x);
}
}
// Generar puntos en extremos de cada fila
for (const auto& [row_y, extremes] : row_extremes) {
result.push_back({extremes.first, static_cast<float>(row_y)}); // Extremo izquierdo
if (extremes.second != extremes.first) { // Solo añadir derecho si es diferente
result.push_back({extremes.second, static_cast<float>(row_y)}); // Extremo derecho
}
}
return result;
}
void PNGShape::update(float delta_time, float screen_width, float screen_height) {
if (!is_flipping_) {
// Estado IDLE: texto de frente con pivoteo sutil (como WAVE_GRID)
idle_timer_ += delta_time;
// Pivoteo sutil constante (movimiento orgánico)
tilt_x_ += 0.4f * delta_time; // Velocidad sutil en X
tilt_y_ += 0.6f * delta_time; // Velocidad sutil en Y
if (idle_timer_ >= next_idle_time_) {
// Iniciar voltereta
is_flipping_ = true;
flip_timer_ = 0.0f;
idle_timer_ = 0.0f;
// Elegir eje aleatorio (0=X, 1=Y, 2=ambos)
flip_axis_ = rand() % 3;
// Próximo tiempo idle aleatorio
next_idle_time_ = PNG_IDLE_TIME_MIN +
(rand() % 1000) / 1000.0f * (PNG_IDLE_TIME_MAX - PNG_IDLE_TIME_MIN);
}
} else {
// Estado FLIP: voltereta en curso
flip_timer_ += delta_time;
// Rotar según eje elegido
if (flip_axis_ == 0 || flip_axis_ == 2) {
angle_x_ += PNG_FLIP_SPEED * delta_time;
}
if (flip_axis_ == 1 || flip_axis_ == 2) {
angle_y_ += PNG_FLIP_SPEED * delta_time;
}
// Terminar voltereta
if (flip_timer_ >= PNG_FLIP_DURATION) {
is_flipping_ = false;
// Resetear ángulos a 0 (volver de frente)
angle_x_ = 0.0f;
angle_y_ = 0.0f;
}
}
}
void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Usar SIEMPRE el vector optimizado (resultado final de generatePoints)
const std::vector<Point2D>& points = optimized_points_;
if (points.empty()) {
x = y = z = 0.0f;
return;
}
// ENFOQUE A: Extrusión 2D
// Cada punto 2D se replica en múltiples capas Z
int num_2d_points = static_cast<int>(points.size());
int point_2d_index = index % num_2d_points;
int layer_index = (index / num_2d_points) % num_layers_;
// Obtener coordenadas 2D del píxel
Point2D p = points[point_2d_index];
// Centrar y escalar
float x_base = (p.x - center_offset_x_) * scale_factor_;
float y_base = (p.y - center_offset_y_) * scale_factor_;
// Calcular Z según capa (distribuir uniformemente en profundidad)
float z_base = 0.0f;
if (num_layers_ > 1) {
float layer_step = extrusion_depth_ / static_cast<float>(num_layers_ - 1);
z_base = -extrusion_depth_ * 0.5f + layer_index * layer_step;
}
// Añadir pivoteo sutil en estado IDLE (similar a WAVE_GRID)
// Calcular tamaño del logo en pantalla para normalizar correctamente
float logo_width = image_width_ * scale_factor_;
float logo_height = image_height_ * scale_factor_;
float logo_size = std::max(logo_width, logo_height);
// Normalizar coordenadas a rango [-1, 1]
float u = x_base / (logo_size * 0.5f);
float v = y_base / (logo_size * 0.5f);
// Calcular pivoteo (amplitudes más grandes, similar a WAVE_GRID)
float tilt_amount_x = sinf(tilt_x_) * 0.15f; // 15% como WAVE_GRID
float tilt_amount_y = sinf(tilt_y_) * 0.1f; // 10% como WAVE_GRID
// Aplicar pivoteo proporcional al tamaño del logo
float z_tilt = (u * tilt_amount_y + v * tilt_amount_x) * logo_size;
z_base += z_tilt; // Añadir pivoteo sutil a la profundidad
// Aplicar rotación en eje Y (horizontal)
float cos_y = cosf(angle_y_);
float sin_y = sinf(angle_y_);
float x_rot_y = x_base * cos_y - z_base * sin_y;
float z_rot_y = x_base * sin_y + z_base * cos_y;
// Aplicar rotación en eje X (vertical)
float cos_x = cosf(angle_x_);
float sin_x = sinf(angle_x_);
float y_rot = y_base * cos_x - z_rot_y * sin_x;
float z_rot = y_base * sin_x + z_rot_y * cos_x;
// Retornar coordenadas finales
x = x_rot_y;
y = y_rot;
// Cuando está de frente (sin rotación), usar Z con pivoteo sutil
if (angle_x_ == 0.0f && angle_y_ == 0.0f) {
// De frente: usar z_base que incluye el pivoteo sutil
z = z_base;
} else {
z = z_rot;
}
}
float PNGShape::getScaleFactor(float screen_height) const {
// Escala dinámica según resolución
return PNG_SIZE_FACTOR;
}