Files
vibe3_physics/source/shapes/png_shape.cpp
Sergio Valor d030d4270e Fix: PNG_SHAPE distribución adaptativa corregida completamente
PROBLEMAS RESUELTOS:
1. getPoint3D() ignoraba optimización → usaba edge_points_ siempre
2. extractAlternateRows() era destructiva → filtraba sobre filtrado
3. Con 10,000 pelotas mostraba bordes → ahora muestra RELLENO
4. Con 100 pelotas solo primera fila → ahora muestra todo el texto

CAMBIOS IMPLEMENTADOS:
- Añadido optimized_points_ (vector resultado final)
- extractAlternateRows() ahora es función pura (devuelve nuevo vector)
- extractCornerVertices() ahora es función pura
- Cada nivel recalcula desde original (no desde filtrado previo)
- getPoint3D() usa optimized_points_ exclusivamente

FLUJO CORRECTO:
- 10,000 pelotas: RELLENO completo (capas reducidas)
- 500 pelotas: RELLENO + filas alternas (texto completo visible)
- 100 pelotas: BORDES completos (todo el texto visible)
- 10 pelotas: VÉRTICES (esqueleto visible)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 17:07:06 +02:00

395 lines
15 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_;
std::cout << "[PNG_SHAPE] Nivel 1: Modo inicial " << mode_name
<< " (puntos 2D: " << num_2d_points << ", capas: " << num_layers_
<< ", total 3D: " << total_3d_points << ")\n";
std::cout << "[PNG_SHAPE] Pelotas disponibles: " << num_points << "\n";
// NIVEL 2: Reducir capas de extrusión PRIMERO (antes de cambiar a bordes)
// Objetivo: Mantener relleno completo pero más fino
while (num_layers_ > 3 && num_points < static_cast<int>(total_3d_points) * 0.8f) {
num_layers_ = std::max(3, num_layers_ / 2);
total_3d_points = num_2d_points * num_layers_;
std::cout << "[PNG_SHAPE] Nivel 2: Reduciendo capas a " << num_layers_
<< " (puntos 3D: " << total_3d_points << ")\n";
}
// NIVEL 3: Filas alternas en RELLENO (mantiene densidad visual)
// 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 < 4 && num_points < static_cast<int>(total_3d_points) * 0.8f) {
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_;
std::cout << "[PNG_SHAPE] Nivel 3: Filas alternas RELLENO (cada " << row_skip
<< " filas, puntos 2D: " << num_2d_points << ", total 3D: " << total_3d_points << ")\n";
mode_name = "RELLENO + FILAS/" + std::to_string(row_skip);
}
}
// NIVEL 4: Cambiar a BORDES solo si relleno optimizado no es suficiente
if (!PNG_USE_EDGES_ONLY && num_points < static_cast<int>(total_3d_points) * 0.5f) {
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
std::cout << "[PNG_SHAPE] Nivel 4: Cambiando a BORDES (pelotas: " << num_points
<< ", necesarias: " << total_3d_points << ")\n";
}
// NIVEL 5: Filas alternas en BORDES (si aún no es suficiente)
while (row_skip < 8 && num_points < static_cast<int>(total_3d_points) * 0.7f) {
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_;
std::cout << "[PNG_SHAPE] Nivel 5: Filas alternas BORDES (cada " << row_skip
<< " filas, puntos 2D: " << num_2d_points << ", total 3D: " << total_3d_points << ")\n";
if (mode_name.find("FILAS") == std::string::npos) {
mode_name += " + FILAS/" + std::to_string(row_skip);
}
}
// NIVEL 6: Reducir más las capas si aún sobran puntos
while (num_layers_ > 1 && num_points < static_cast<int>(total_3d_points) * 0.6f) {
num_layers_ = std::max(1, num_layers_ / 2);
total_3d_points = num_2d_points * num_layers_;
std::cout << "[PNG_SHAPE] Nivel 6: Reduciendo más capas a " << num_layers_
<< " (puntos 3D: " << total_3d_points << ")\n";
}
// NIVEL 7: Vértices/esquinas (último recurso, muy pocas pelotas)
if (num_points < static_cast<int>(total_3d_points) * 0.4f && num_points < 100) {
// 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";
std::cout << "[PNG_SHAPE] Nivel 7: Solo vértices (puntos 2D: " << num_2d_points << ")\n";
}
}
// ✅ CLAVE: Guardar el conjunto de puntos optimizado final en optimized_points_ (usado por getPoint3D)
optimized_points_ = active_points_data;
// Debug: mostrar configuración final
std::cout << "[PNG_SHAPE] === CONFIGURACIÓN FINAL ===\n";
std::cout << "[PNG_SHAPE] Modo: " << mode_name << "\n";
std::cout << "[PNG_SHAPE] Píxeles 2D: " << num_2d_points << "\n";
std::cout << "[PNG_SHAPE] Capas extrusión: " << num_layers_ << "\n";
std::cout << "[PNG_SHAPE] Total puntos 3D: " << total_3d_points << "\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;
}
// 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
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 {
// 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;
}
// 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;
}