merge: issue/0029 — mesh_viewer + obj loader + orbit_camera

# Conflicts:
#	cpp/apps/primitives_gallery/demos.h
#	cpp/apps/primitives_gallery/main.cpp
This commit is contained in:
2026-04-25 21:54:27 +02:00
19 changed files with 1113 additions and 0 deletions
@@ -0,0 +1,178 @@
# 0029 — C++ mesh_viewer + obj loader + orbit_camera
## APP Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0029 |
| **Estado** | pendiente |
| **Prioridad** | media |
| **Tipo** | feature — C++ viz/gfx (cpp/functions/viz, cpp/functions/gfx) |
## Dependencias
`gl_loader_cpp_gfx`, `gl_shader_cpp_gfx`, `gl_framebuffer_cpp_gfx`. Independiente de los demas issues.
**Desbloquea:** visualizacion 3D real (no solo plots): inspeccionar modelos, point clouds, debugging de geometria.
---
## Objetivo
Tres primitivos:
1. **`mesh_obj_load_cpp_gfx`** — parser minimo de Wavefront `.obj` (vertices + normales + indices). Sin materiales ni texturas en este issue.
2. **`orbit_camera_cpp_core`** — camara orbital con drag (azimuth/elevation/distance), uniforms `view`/`proj`. Estado puro + helpers.
3. **`mesh_viewer_cpp_viz`** — componente ImGui que rendea una malla `MeshGpu` con orbit camera dentro de un FBO + `ImGui::Image`.
Demo en `primitives_gallery` con un cubo procedural y opcion de cargar un `.obj` desde disco.
## Contexto
El stack actual hace 2D plotting (ImPlot) y 2D fragment shaders (`shader_canvas`). No hay forma de visualizar geometria 3D. ImPlot3D (issue 0028) cubre plots cientificos pero no meshes generales. Este issue añade el camino "raster 3D" autonomo.
## Arquitectura
```
cpp/functions/gfx/
├── mesh_obj_load.h # NEW
├── mesh_obj_load.cpp # NEW (parser puro)
├── mesh_obj_load.md # NEW (kind: function, purity: pure)
├── mesh_gpu.h # NEW (VAO/VBO/IBO de un Mesh)
├── mesh_gpu.cpp # NEW
└── mesh_gpu.md # NEW (kind: function, purity: impure)
cpp/functions/core/
├── orbit_camera.h # NEW
├── orbit_camera.cpp # NEW
└── orbit_camera.md # NEW (kind: function, purity: pure)
cpp/functions/viz/
├── mesh_viewer.h # NEW
├── mesh_viewer.cpp # NEW
└── mesh_viewer.md # NEW (kind: component, purity: impure)
cpp/apps/primitives_gallery/
├── demos_mesh.cpp # NEW
├── demos.h # MOD
├── main.cpp # MOD
└── CMakeLists.txt # MOD
cpp/CMakeLists.txt # MOD
```
### API propuesta
```cpp
namespace fn {
// --- mesh_obj_load (puro) ---
struct Mesh {
std::vector<float> positions; // x,y,z stride=3
std::vector<float> normals; // optional, stride=3
std::vector<uint32_t> indices;
};
Mesh mesh_obj_parse(const char* obj_text, size_t len); // pure
Mesh mesh_obj_load(const char* path); // impure (lee fichero) — vive en mesh_gpu.cpp o aparte
// --- mesh_gpu (impure) ---
struct MeshGpu {
GLuint vao = 0, vbo = 0, ebo = 0;
int index_count = 0;
bool ok() const { return vao != 0; }
};
MeshGpu mesh_gpu_upload(const Mesh&);
void mesh_gpu_destroy(MeshGpu&);
// --- orbit_camera (puro) ---
struct OrbitCamera {
float azimuth = 0.7f; // rad
float elevation = 0.4f; // rad
float distance = 3.0f;
float fov = 45.0f; // deg
float aspect = 1.0f;
float near_plane = 0.05f;
float far_plane = 100.0f;
};
struct CameraMatrices { float view[16]; float proj[16]; };
CameraMatrices orbit_camera_matrices(const OrbitCamera&);
void orbit_camera_handle_drag(OrbitCamera&, ImVec2 drag_delta, float wheel);
// --- mesh_viewer (impure) ---
struct MeshViewerConfig {
const MeshGpu* mesh;
OrbitCamera* cam; // se modifica con drag
ImVec2 size = {-1, 400};
ImU32 color = IM_COL32(180,180,200,255);
bool wireframe = false;
};
void mesh_viewer(const char* id, const MeshViewerConfig&);
}
```
`mesh_viewer` internamente:
1. Compila/cachea un shader de Lambert minimo (vertex + fragment).
2. Tiene un `Framebuffer` propio (cache por `id` + tamaño).
3. Cada frame: bind FBO, draw mesh, `ImGui::Image(framebuffer.color_tex)`.
4. Si `IsItemActive()` y mouse drag → `orbit_camera_handle_drag`.
## Tareas
### Fase 1 — mesh_obj_load (puro)
- 1.1 Implementar parser que cubre `v`, `vn`, `f` (tris y quads). Ignora `vt`, `mtllib`, materiales en este issue.
- 1.2 Generar normales por face si faltan.
- 1.3 Tests unitarios con .obj inline (cubo).
- 1.4 `.md` con frontmatter.
### Fase 2 — mesh_gpu (impuro)
- 2.1 `mesh_gpu_upload`: crea VAO + VBO interleaved (pos+normal) + EBO.
- 2.2 `.md` con frontmatter.
### Fase 3 — orbit_camera (puro)
- 3.1 Calcular `view = lookAt(eye, target=0, up=Y)` + `proj = perspective`.
- 3.2 `handle_drag`: drag.x → azimuth, drag.y → elevation (clamp ±π/2 - eps), wheel → distance (clamp >0).
- 3.3 Tests unitarios para matrices (idempotencia drag=0, rango de elevation).
- 3.4 `.md`.
### Fase 4 — mesh_viewer
- 4.1 Implementar el componente con FBO interno cacheado por `id`.
- 4.2 Shader de iluminacion Lambert con luz fija desde la camara.
- 4.3 `.md`.
### Fase 5 — Gallery demo
- 5.1 `demos_mesh.cpp`: dos sub-demos: cubo procedural (generado in-line) + boton "Load .obj…" con path absoluto en text input.
- 5.2 Registrar en gallery.
### Fase 6 — Tests + docs
- 6.1 Test parser obj (cubo: 8 vertices, 12 tris).
- 6.2 Test orbit_camera (matrices conocidas).
- 6.3 `./fn index` + `./fn show` para los nuevos.
## Ejemplo de uso
```cpp
auto mesh = fn::mesh_obj_load("assets/teapot.obj");
auto gpu = fn::mesh_gpu_upload(mesh);
fn::OrbitCamera cam;
cam.aspect = 4.0f/3.0f;
fn::run_app("mesh demo", [&]{
fn::MeshViewerConfig cfg{};
cfg.mesh = &gpu; cfg.cam = &cam;
fn::mesh_viewer("##mv", cfg);
});
```
## Decisiones de diseño
- **Sin glm**: matrices a mano (4×4 row-major). Evita dependencia extra; el codigo es ~50 LOC.
- **Sin gltf por ahora**: .obj cubre el 80% de casos de inspeccion rapida. gltf en issue futuro si se necesita.
- **Iluminacion fija**: Lambert con luz=camara (headlight). Suficiente para inspeccion de geometria.
## Riesgos
- **Obj con quads o n-gons**: cubrir tris y quads (tris-fan), advertir en .md.
- **Modelos enormes**: limite practico 1M tris. Documentar.
- **Cache de FBO por id**: si la app cambia el id dinamicamente, fugas. Documentar para reusar `id` estable.