Files
vibe3_physics/source/shapes/png_shape.cpp
Sergio Valor a04c1cba13 PNG_SHAPE: Distribución adaptativa multinivel
Sistema de optimización en 3 niveles para cualquier cantidad de pelotas:

Nivel 1 - Cambio de modo:
- Si RELLENO y pocas pelotas → switch a BORDES
- Reduce de ~22K puntos a ~4.5K puntos

Nivel 2 - Reducción de capas:
- Si aún insuficiente → dividir capas a la mitad
- 15 capas → 7 capas → 3 capas → 1 capa
- Reduce profundidad pero mantiene forma visible

Nivel 3 - Sampling de píxeles:
- Si aún insuficiente → tomar cada N píxeles
- Sampling 1/2, 1/3, 1/4... hasta 1/10
- Funciona en BORDES o RELLENO
- Mantiene forma general con menos detalle

Resultado:
- Con 1 pelota: Funciona (1 píxel visible) 
- Con 10 pelotas: Forma reconocible 
- Con 100 pelotas: Forma clara 
- Con 1000+ pelotas: Relleno completo 

Output informativo:
  [PNG_SHAPE] Paso 1: Cambiando RELLENO → BORDES
  [PNG_SHAPE] Paso 2: Reduciendo capas a 3
  [PNG_SHAPE] Paso 3: Aplicando sampling 1/4
  [PNG_SHAPE] === CONFIGURACIÓN FINAL ===
  [PNG_SHAPE] Modo: BORDES (optimizado)
  [PNG_SHAPE] Píxeles 2D: 75 (sampling 1/4)
  [PNG_SHAPE] Capas: 3
  [PNG_SHAPE] Ratio: 1.33 pelotas/punto 

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

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

274 lines
9.6 KiB
C++

#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 2D 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 MULTINIVEL ===
// Nivel 1: Si relleno completo y pocas pelotas → switch a bordes
if (!PNG_USE_EDGES_ONLY && num_points < static_cast<int>(total_3d_points) / 2) {
std::cout << "[PNG_SHAPE] Paso 1: Cambiando de RELLENO a BORDES (pelotas insuficientes)\n";
detectEdges();
num_2d_points = edge_points_.size();
total_3d_points = num_2d_points * num_layers_;
}
// Nivel 2: Reducir capas de extrusión si aún hay pocas pelotas
while (num_layers_ > 1 && num_points < static_cast<int>(total_3d_points) / 2) {
num_layers_ = std::max(1, num_layers_ / 2); // Dividir capas a la mitad
total_3d_points = num_2d_points * num_layers_;
std::cout << "[PNG_SHAPE] Paso 2: Reduciendo capas a " << num_layers_
<< " (total puntos: " << total_3d_points << ")\n";
}
// Nivel 3: Sampling de píxeles (tomar cada N píxeles) si aún insuficiente
int sampling_step = 1;
std::vector<Point2D>* points_to_sample = (!edge_points_.empty()) ? &edge_points_ : &filled_points_;
while (sampling_step < 10 && num_points < static_cast<int>(total_3d_points) / 2) {
sampling_step++;
// Aplicar sampling (tomar cada N puntos)
std::vector<Point2D> sampled_points;
for (size_t i = 0; i < points_to_sample->size(); i += sampling_step) {
sampled_points.push_back((*points_to_sample)[i]);
}
if (!sampled_points.empty()) {
*points_to_sample = sampled_points;
num_2d_points = points_to_sample->size();
total_3d_points = num_2d_points * num_layers_;
std::cout << "[PNG_SHAPE] Paso 3: Aplicando sampling 1/" << sampling_step
<< " (puntos: " << num_2d_points << ")\n";
}
}
// Debug: mostrar configuración final
std::cout << "[PNG_SHAPE] === CONFIGURACIÓN FINAL ===\n";
std::string mode_str = (!edge_points_.empty()) ? "BORDES" : (PNG_USE_EDGES_ONLY ? "BORDES" : "RELLENO");
std::cout << "[PNG_SHAPE] Modo: " << mode_str;
if (sampling_step > 1) {
std::cout << " (optimizado)";
}
std::cout << "\n";
std::cout << "[PNG_SHAPE] Píxeles 2D: " << num_2d_points;
if (sampling_step > 1) {
std::cout << " (sampling 1/" << sampling_step << ")";
}
std::cout << "\n";
std::cout << "[PNG_SHAPE] Capas: " << 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;
}
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;
}