--- 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 positions; // x,y,z stride=3 std::vector normals; // optional, stride=3 std::vector 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.