Files
vibe3_physics/source/shapes/png_shape.cpp
Sergio Valor 757bb9c525 PNG_SHAPE: Auto-switch a bordes cuando hay pocas pelotas
Problema:
- PNG_USE_EDGES_ONLY = false usa ~22,000 puntos 3D
- Con 1, 10 o 100 pelotas, no hay suficientes para formar el logo
- Resultado: logo invisible o mal formado

Solución:
- Detectar automáticamente si num_pelotas < total_puntos / 2
- Si hay pocas pelotas → cambiar automáticamente a BORDES
- Bordes usa ~300 puntos × 15 capas = ~4,500 puntos 3D
- Mucho mejor ratio para pocos sprites

Implementación:
- generatePoints() verifica ratio pelotas/puntos
- Si insuficiente: llama detectEdges() y regenera
- getPoint3D() usa edge_points_ si están disponibles
- Mensajes informativos en consola

Ahora funciona:
  Escenario 1 (1 pelota) → Auto-switch a bordes 
  Escenario 2 (10 pelotas) → Auto-switch a bordes 
  Escenario 5 (1000 pelotas) → Usa relleno completo 
  Escenario 6+ (10K+ pelotas) → Usa relleno completo 

Output de debug muestra:
  [PNG_SHAPE] Advertencia: Solo X pelotas para Y puntos
  [PNG_SHAPE] Cambiando automáticamente a BORDES...
  [PNG_SHAPE] Modo: BORDES/RELLENO

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 16:42:27 +02:00

235 lines
8.0 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "png_shape.h"
#include "../defines.h"
#include "../external/stb_image.h"
#include <cmath>
#include <algorithm>
#include <iostream>
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 puntos según el enfoque configurado
generateExtrudedPoints();
// Calcular cuántos puntos 3D se necesitan
size_t num_2d_points = PNG_USE_EDGES_ONLY ? edge_points_.size() : filled_points_.size();
size_t total_3d_points = num_2d_points * num_layers_;
// ADAPTACIÓN AUTOMÁTICA: Si hay muy pocas pelotas, cambiar a bordes
if (!PNG_USE_EDGES_ONLY && num_points < static_cast<int>(total_3d_points) / 2) {
std::cout << "[PNG_SHAPE] Advertencia: Solo " << num_points << " pelotas para "
<< total_3d_points << " puntos (relleno).\n";
std::cout << "[PNG_SHAPE] Cambiando automáticamente a BORDES para mejor visualización.\n";
// Regenerar solo con bordes
detectEdges();
num_2d_points = edge_points_.size();
total_3d_points = num_2d_points * num_layers_;
}
// Debug: mostrar configuración final
std::cout << "[PNG_SHAPE] Modo: " << (PNG_USE_EDGES_ONLY ? "BORDES" : "RELLENO") << "\n";
std::cout << "[PNG_SHAPE] Detectados " << num_2d_points << " puntos 2D × "
<< num_layers_ << " capas = " << total_3d_points << " puntos 3D totales\n";
std::cout << "[PNG_SHAPE] Pelotas disponibles: " << num_points << "\n";
std::cout << "[PNG_SHAPE] Ratio: " << (float)num_points / (float)total_3d_points << " pelotas/punto\n";
// 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;
}
void PNGShape::update(float delta_time, float screen_width, float screen_height) {
if (!is_flipping_) {
// Estado IDLE: texto de frente
idle_timer_ += delta_time;
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 {
// Seleccionar puntos según configuración (priorizar edges si fue auto-ajustado)
const std::vector<Point2D>& points = (!edge_points_.empty()) ? edge_points_ : filled_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;
}
// 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), forzar Z positivo (primer plano brillante)
if (angle_x_ == 0.0f && angle_y_ == 0.0f) {
// De frente: todo en primer plano (Z máximo)
z = extrusion_depth_ * 0.5f; // Máximo Z = más brillante
} else {
z = z_rot;
}
}
float PNGShape::getScaleFactor(float screen_height) const {
// Escala dinámica según resolución
return PNG_SIZE_FACTOR;
}