Files
fn_registry/dev/issues/completed/0029-cpp-mesh-viewer.md
T

195 lines
6.4 KiB
Markdown
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.
---
id: "0029"
title: "C++ mesh_viewer + obj loader + orbit_camera"
status: completado
type: feature
domain:
- cpp-stack
scope: multi-app
priority: media
depends: []
blocks: []
related: []
created: 2026-05-17
updated: 2026-05-17
tags: []
---
# 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.