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>
274 lines
9.6 KiB
C++
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;
|
|
}
|