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:
@@ -9,6 +9,7 @@ add_imgui_app(primitives_gallery
|
||||
demos_text_editor.cpp
|
||||
demos_gl_texture.cpp
|
||||
demos_extras.cpp
|
||||
demos_mesh.cpp
|
||||
# text_editor + file_watcher (issue 0025)
|
||||
${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp
|
||||
@@ -59,6 +60,11 @@ add_imgui_app(primitives_gallery
|
||||
# gl_texture_load (issue 0026) + stb_image
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/gl_texture_load.cpp
|
||||
${CMAKE_SOURCE_DIR}/vendor/stb/stb_image_impl.cpp
|
||||
# mesh_viewer stack (issue 0029)
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/mesh_obj_load.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/mesh_gpu.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/orbit_camera.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/mesh_viewer.cpp
|
||||
)
|
||||
target_include_directories(primitives_gallery PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit
|
||||
|
||||
@@ -36,6 +36,7 @@ void demo_heatmap();
|
||||
void demo_table_view();
|
||||
void demo_surface_plot_3d(); // issue 0028, ImPlot3D
|
||||
void demo_scatter_3d(); // issue 0028, ImPlot3D
|
||||
void demo_mesh_viewer(); // issue 0029
|
||||
|
||||
// --- Gfx ---
|
||||
void demo_shader_canvas();
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// Demo del primitivo viz/mesh_viewer.
|
||||
// Genera un cubo procedural in-line, lo sube al GPU, y permite cargar un
|
||||
// .obj desde un path ingresado en un text input.
|
||||
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "viz/mesh_viewer.h"
|
||||
#include "gfx/mesh_obj_load.h"
|
||||
#include "gfx/mesh_gpu.h"
|
||||
#include "core/orbit_camera.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
namespace {
|
||||
|
||||
const char* kCubeObj =
|
||||
"v -1 -1 -1\nv 1 -1 -1\nv 1 1 -1\nv -1 1 -1\n"
|
||||
"v -1 -1 1\nv 1 -1 1\nv 1 1 1\nv -1 1 1\n"
|
||||
"f 4 3 2 1\n" // back (-Z) — winding for outward normal
|
||||
"f 5 6 7 8\n" // front (+Z)
|
||||
"f 1 2 6 5\n" // bottom (-Y)
|
||||
"f 8 7 3 4\n" // top (+Y)
|
||||
"f 5 8 4 1\n" // left (-X)
|
||||
"f 2 3 7 6\n"; // right (+X)
|
||||
|
||||
struct State {
|
||||
fn::gfx::MeshGpu mesh{};
|
||||
fn::core::OrbitCamera cam{};
|
||||
char path[512] = "";
|
||||
std::string status;
|
||||
bool wireframe = false;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
State& state() {
|
||||
static State s;
|
||||
return s;
|
||||
}
|
||||
|
||||
void load_cube() {
|
||||
auto& s = state();
|
||||
if (s.mesh.ok()) fn::gfx::mesh_gpu_destroy(s.mesh);
|
||||
auto cpu = fn::gfx::mesh_obj_parse(kCubeObj, std::strlen(kCubeObj));
|
||||
s.mesh = fn::gfx::mesh_gpu_upload(cpu);
|
||||
s.status = s.mesh.ok()
|
||||
? ("loaded cube: " + std::to_string(s.mesh.index_count / 3) + " tris")
|
||||
: "cube upload failed";
|
||||
}
|
||||
|
||||
void load_from_path() {
|
||||
auto& s = state();
|
||||
if (!s.path[0]) { s.status = "path is empty"; return; }
|
||||
auto cpu = fn::gfx::mesh_obj_load(s.path);
|
||||
if (cpu.positions.empty()) { s.status = "parse/read failed"; return; }
|
||||
if (s.mesh.ok()) fn::gfx::mesh_gpu_destroy(s.mesh);
|
||||
s.mesh = fn::gfx::mesh_gpu_upload(cpu);
|
||||
s.status = s.mesh.ok()
|
||||
? ("loaded: " + std::to_string(s.mesh.index_count / 3) + " tris")
|
||||
: "upload failed";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void demo_mesh_viewer() {
|
||||
demo_header("mesh_viewer", "v1.0.0",
|
||||
"Visualizador 3D para inspeccion de geometria. Composicion de "
|
||||
"mesh_obj_load (parser .obj puro) + mesh_gpu (upload VAO/VBO/EBO) + "
|
||||
"orbit_camera (drag/wheel) + mesh_viewer (FBO + ImGui::Image + Lambert).");
|
||||
|
||||
auto& s = state();
|
||||
if (!s.initialized) {
|
||||
load_cube();
|
||||
s.initialized = true;
|
||||
}
|
||||
|
||||
// Controls row.
|
||||
if (ImGui::Button("Reload cube")) load_cube();
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Wireframe", &s.wireframe);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("|");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(360);
|
||||
ImGui::InputTextWithHint("##obj_path", "absolute path to .obj", s.path, sizeof(s.path));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Load .obj")) load_from_path();
|
||||
|
||||
ImGui::TextDisabled("status: %s | tris: %d | drag to orbit, wheel to zoom",
|
||||
s.status.c_str(),
|
||||
s.mesh.ok() ? s.mesh.index_count / 3 : 0);
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
fn::viz::MeshViewerConfig cfg{};
|
||||
cfg.mesh = &s.mesh;
|
||||
cfg.cam = &s.cam;
|
||||
cfg.size = ImVec2(-1.0f, 480.0f);
|
||||
cfg.color = IM_COL32(160, 200, 255, 255);
|
||||
cfg.wireframe = s.wireframe;
|
||||
fn::viz::mesh_viewer("##gallery_mesh_viewer", cfg);
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
@@ -63,6 +63,7 @@ static const DemoEntry k_demos[] = {
|
||||
{"table_view", "table_view", "Viz", &gallery::demo_table_view},
|
||||
{"surface_plot_3d", "surface_plot_3d", "Viz", &gallery::demo_surface_plot_3d},
|
||||
{"scatter_3d", "scatter_3d", "Viz", &gallery::demo_scatter_3d},
|
||||
{"mesh_viewer", "mesh_viewer", "Viz", &gallery::demo_mesh_viewer},
|
||||
// Gfx (shaders_lab core)
|
||||
{"shader_canvas", "shader_canvas", "Gfx", &gallery::demo_shader_canvas},
|
||||
{"gl_texture", "gl_texture_load", "Gfx", &gallery::demo_gl_texture}, // wave 1
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#include "core/orbit_camera.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace fn::core {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kPi = 3.14159265358979323846f;
|
||||
|
||||
// row-major identity
|
||||
void mat4_identity(float m[16]) {
|
||||
for (int i = 0; i < 16; ++i) m[i] = 0.0f;
|
||||
m[0] = m[5] = m[10] = m[15] = 1.0f;
|
||||
}
|
||||
|
||||
// row-major: m[row*4 + col]
|
||||
void make_lookat(float view[16],
|
||||
float ex, float ey, float ez,
|
||||
float cx, float cy, float cz,
|
||||
float ux, float uy, float uz) {
|
||||
// forward = normalize(center - eye)
|
||||
float fx = cx - ex, fy = cy - ey, fz = cz - ez;
|
||||
float fl = std::sqrt(fx*fx + fy*fy + fz*fz);
|
||||
if (fl > 0) { fx /= fl; fy /= fl; fz /= fl; }
|
||||
// side = normalize(cross(forward, up))
|
||||
float sx = fy*uz - fz*uy;
|
||||
float sy = fz*ux - fx*uz;
|
||||
float sz = fx*uy - fy*ux;
|
||||
float sl = std::sqrt(sx*sx + sy*sy + sz*sz);
|
||||
if (sl > 0) { sx /= sl; sy /= sl; sz /= sl; }
|
||||
// up' = cross(side, forward)
|
||||
float ux2 = sy*fz - sz*fy;
|
||||
float uy2 = sz*fx - sx*fz;
|
||||
float uz2 = sx*fy - sy*fx;
|
||||
|
||||
mat4_identity(view);
|
||||
view[0] = sx; view[1] = sy; view[2] = sz; view[3] = -(sx*ex + sy*ey + sz*ez);
|
||||
view[4] = ux2; view[5] = uy2; view[6] = uz2; view[7] = -(ux2*ex + uy2*ey + uz2*ez);
|
||||
view[8] = -fx; view[9] = -fy; view[10] = -fz; view[11] = (fx*ex + fy*ey + fz*ez);
|
||||
view[12] = 0; view[13] = 0; view[14] = 0; view[15] = 1;
|
||||
}
|
||||
|
||||
void make_perspective(float proj[16], float fov_deg, float aspect, float n, float f) {
|
||||
float fov_rad = fov_deg * kPi / 180.0f;
|
||||
float t = 1.0f / std::tan(fov_rad * 0.5f);
|
||||
for (int i = 0; i < 16; ++i) proj[i] = 0.0f;
|
||||
proj[0] = t / aspect;
|
||||
proj[5] = t;
|
||||
proj[10] = (f + n) / (n - f);
|
||||
proj[11] = (2.0f * f * n) / (n - f);
|
||||
proj[14] = -1.0f;
|
||||
proj[15] = 0.0f;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CameraMatrices orbit_camera_matrices(const OrbitCamera& cam) {
|
||||
CameraMatrices m{};
|
||||
// Convert spherical (azimuth, elevation, distance) -> cartesian eye position.
|
||||
// azimuth rotates around Y; elevation tilts up/down.
|
||||
float ce = std::cos(cam.elevation);
|
||||
float se = std::sin(cam.elevation);
|
||||
float ca = std::cos(cam.azimuth);
|
||||
float sa = std::sin(cam.azimuth);
|
||||
float ex = cam.distance * ce * sa;
|
||||
float ey = cam.distance * se;
|
||||
float ez = cam.distance * ce * ca;
|
||||
|
||||
make_lookat(m.view, ex, ey, ez, 0, 0, 0, 0, 1, 0);
|
||||
make_perspective(m.proj, cam.fov, cam.aspect, cam.near_plane, cam.far_plane);
|
||||
return m;
|
||||
}
|
||||
|
||||
void orbit_camera_handle_drag(OrbitCamera& cam, ImVec2 drag_delta, float wheel) {
|
||||
constexpr float kDragSens = 0.01f;
|
||||
cam.azimuth += drag_delta.x * kDragSens;
|
||||
cam.elevation += drag_delta.y * kDragSens;
|
||||
|
||||
// clamp elevation to ±π/2 - eps to avoid gimbal flip
|
||||
constexpr float kElevMax = kPi * 0.5f - 1e-3f;
|
||||
if (cam.elevation > kElevMax) cam.elevation = kElevMax;
|
||||
if (cam.elevation < -kElevMax) cam.elevation = -kElevMax;
|
||||
|
||||
if (wheel != 0.0f) {
|
||||
cam.distance *= (1.0f - wheel * 0.1f);
|
||||
if (cam.distance < 0.1f) cam.distance = 0.1f;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fn::core
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
// orbit_camera — camara orbital pura. Estado minimo (azimuth, elevation,
|
||||
// distance, fov, aspect, near/far) y dos helpers: matrices view/proj y
|
||||
// handle_drag para integrar drag/wheel del mouse.
|
||||
//
|
||||
// Las matrices se devuelven como float[16] row-major, pero al subirlas a
|
||||
// OpenGL con glUniformMatrix4fv hay que pasar transpose=GL_TRUE (porque
|
||||
// GL espera column-major). Ver mesh_viewer.cpp para el patron.
|
||||
|
||||
struct ImVec2; // fwd-decl, definido por imgui.h
|
||||
|
||||
namespace fn::core {
|
||||
|
||||
struct OrbitCamera {
|
||||
float azimuth = 0.7f; // rad, rotacion en plano XZ
|
||||
float elevation = 0.4f; // rad, rotacion en plano YZ, clamp ±π/2-eps
|
||||
float distance = 3.0f; // distancia al origen (target=0,0,0)
|
||||
float fov = 45.0f; // grados, vertical FOV
|
||||
float aspect = 1.0f; // width/height del viewport
|
||||
float near_plane = 0.05f;
|
||||
float far_plane = 100.0f;
|
||||
};
|
||||
|
||||
struct CameraMatrices {
|
||||
float view[16]; // row-major
|
||||
float proj[16]; // row-major
|
||||
};
|
||||
|
||||
// Calcula view (lookAt eye→origin, up=Y) y proj (perspective). Pure.
|
||||
CameraMatrices orbit_camera_matrices(const OrbitCamera& cam);
|
||||
|
||||
// Aplica un drag de mouse (dx,dy) y rueda (wheel) sobre la camara.
|
||||
// dx → azimuth (sensibilidad 0.01 rad/px)
|
||||
// dy → elevation (clamp a ±π/2 - 1e-3)
|
||||
// wheel → distance *= (1 - wheel*0.1), clamp >0.1
|
||||
// Pure (sin side effects mas alla de mutar la struct in-place).
|
||||
void orbit_camera_handle_drag(OrbitCamera& cam, ImVec2 drag_delta, float wheel);
|
||||
|
||||
} // namespace fn::core
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: orbit_camera
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "CameraMatrices orbit_camera_matrices(const OrbitCamera&); void orbit_camera_handle_drag(OrbitCamera&, ImVec2 drag_delta, float wheel)"
|
||||
description: "Camara orbital pura — estado minimal (azimuth, elevation, distance, fov, aspect, near/far). Helpers: matrices view/proj (row-major float[16]) y handle_drag para integrar drag/wheel."
|
||||
tags: [camera, orbit, math, matrix, lookat, perspective, 3d]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: [imgui]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/core/orbit_camera.cpp"
|
||||
framework: imgui
|
||||
params:
|
||||
- name: cam
|
||||
desc: "OrbitCamera in/out: azimuth (rad), elevation (rad clamped ±π/2-eps), distance, fov (deg), aspect, near/far"
|
||||
- name: drag_delta
|
||||
desc: "ImVec2 desplazamiento del mouse desde la ultima frame (px). dx→azimuth, dy→elevation con sensibilidad 0.01 rad/px"
|
||||
- name: wheel
|
||||
desc: "Float scroll wheel (Imgui::GetIO().MouseWheel). distance *= (1 - wheel*0.1), clamp >0.1"
|
||||
output: "orbit_camera_matrices: CameraMatrices con view (lookAt eye→origin, up=Y) y proj (perspective). Ambas row-major; al pasar a glUniformMatrix4fv usar transpose=GL_TRUE. orbit_camera_handle_drag: muta cam in-place, sin allocacion."
|
||||
---
|
||||
|
||||
# orbit_camera
|
||||
|
||||
Camara orbital trivial para inspeccionar geometria 3D. Mantiene un objetivo fijo en el origen y rota la posicion del ojo en una esfera de radio `distance`.
|
||||
|
||||
## Convenciones
|
||||
|
||||
- **Azimuth = 0, elevation = 0** → ojo en `+Z`, mirando hacia `-Z`.
|
||||
- **Up vector** = `+Y`. Right-handed.
|
||||
- **Matrices row-major**: al subir a OpenGL, usar `glUniformMatrix4fv(loc, 1, GL_TRUE, &m.view[0])`.
|
||||
- Sin glm: ~50 LOC, sin dependencias mas alla de `<cmath>`.
|
||||
|
||||
## Uso tipico
|
||||
|
||||
```cpp
|
||||
fn::core::OrbitCamera cam;
|
||||
cam.aspect = (float)w / (float)h;
|
||||
|
||||
// En el render loop:
|
||||
auto m = fn::core::orbit_camera_matrices(cam);
|
||||
glUniformMatrix4fv(loc_view, 1, GL_TRUE, m.view);
|
||||
glUniformMatrix4fv(loc_proj, 1, GL_TRUE, m.proj);
|
||||
|
||||
// Si el panel esta activo:
|
||||
if (ImGui::IsItemActive()) {
|
||||
fn::core::orbit_camera_handle_drag(
|
||||
cam,
|
||||
ImGui::GetIO().MouseDelta,
|
||||
ImGui::GetIO().MouseWheel
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- Elevation se clampea a `±π/2 - 1e-3` para evitar flip de la up vector cuando se mira desde polos.
|
||||
- Distance minima = 0.1 (impide cruzar el origen con el wheel).
|
||||
- Si necesitas un target distinto al origen, suma `target` a `eye` y pasa `target` a la lookAt — el helper actual mira fijo a `(0,0,0)` por simplicidad.
|
||||
@@ -31,6 +31,7 @@ PFNGLUNIFORM2FPROC fn_glUniform2f = nullptr;
|
||||
PFNGLUNIFORM3FPROC fn_glUniform3f = nullptr;
|
||||
PFNGLUNIFORM4FPROC fn_glUniform4f = nullptr;
|
||||
PFNGLUNIFORM4FVPROC fn_glUniform4fv = nullptr;
|
||||
PFNGLUNIFORMMATRIX4FVPROC fn_glUniformMatrix4fv = nullptr;
|
||||
PFNGLUSEPROGRAMPROC fn_glUseProgram = nullptr;
|
||||
PFNGLACTIVETEXTUREPROC fn_glActiveTexture = nullptr;
|
||||
PFNGLGENERATEMIPMAPPROC fn_glGenerateMipmap = nullptr;
|
||||
@@ -82,6 +83,7 @@ bool gl_loader_init() {
|
||||
LOAD(glUniform3f);
|
||||
LOAD(glUniform4f);
|
||||
LOAD(glUniform4fv);
|
||||
LOAD(glUniformMatrix4fv);
|
||||
LOAD(glUseProgram);
|
||||
LOAD(glActiveTexture);
|
||||
LOAD(glGenerateMipmap);
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
extern PFNGLUNIFORM3FPROC fn_glUniform3f;
|
||||
extern PFNGLUNIFORM4FPROC fn_glUniform4f;
|
||||
extern PFNGLUNIFORM4FVPROC fn_glUniform4fv;
|
||||
extern PFNGLUNIFORMMATRIX4FVPROC fn_glUniformMatrix4fv;
|
||||
extern PFNGLUSEPROGRAMPROC fn_glUseProgram;
|
||||
// Texture (gl_texture_load — issue 0026)
|
||||
extern PFNGLACTIVETEXTUREPROC fn_glActiveTexture;
|
||||
@@ -84,6 +85,7 @@
|
||||
#define glUniform3f fn_glUniform3f
|
||||
#define glUniform4f fn_glUniform4f
|
||||
#define glUniform4fv fn_glUniform4fv
|
||||
#define glUniformMatrix4fv fn_glUniformMatrix4fv
|
||||
#define glUseProgram fn_glUseProgram
|
||||
#define glActiveTexture fn_glActiveTexture
|
||||
#define glGenerateMipmap fn_glGenerateMipmap
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#include "gfx/mesh_gpu.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace fn::gfx {
|
||||
|
||||
MeshGpu mesh_gpu_upload(const Mesh& mesh) {
|
||||
MeshGpu g{};
|
||||
if (mesh.positions.empty() || mesh.indices.empty()) return g;
|
||||
if (mesh.normals.size() != mesh.positions.size()) return g;
|
||||
|
||||
const size_t nverts = mesh.positions.size() / 3;
|
||||
|
||||
// Interleave pos.xyz + normal.xyz, stride 6 floats.
|
||||
std::vector<float> interleaved;
|
||||
interleaved.resize(nverts * 6);
|
||||
for (size_t i = 0; i < nverts; ++i) {
|
||||
interleaved[i*6 + 0] = mesh.positions[i*3 + 0];
|
||||
interleaved[i*6 + 1] = mesh.positions[i*3 + 1];
|
||||
interleaved[i*6 + 2] = mesh.positions[i*3 + 2];
|
||||
interleaved[i*6 + 3] = mesh.normals[i*3 + 0];
|
||||
interleaved[i*6 + 4] = mesh.normals[i*3 + 1];
|
||||
interleaved[i*6 + 5] = mesh.normals[i*3 + 2];
|
||||
}
|
||||
|
||||
glGenVertexArrays(1, &g.vao);
|
||||
glGenBuffers(1, &g.vbo);
|
||||
glGenBuffers(1, &g.ebo);
|
||||
|
||||
glBindVertexArray(g.vao);
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, g.vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER,
|
||||
(GLsizeiptr)(interleaved.size() * sizeof(float)),
|
||||
interleaved.data(),
|
||||
GL_STATIC_DRAW);
|
||||
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g.ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
|
||||
(GLsizeiptr)(mesh.indices.size() * sizeof(uint32_t)),
|
||||
mesh.indices.data(),
|
||||
GL_STATIC_DRAW);
|
||||
|
||||
// location 0 = a_pos (vec3)
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
|
||||
(GLsizei)(6 * sizeof(float)),
|
||||
(const void*)0);
|
||||
// location 1 = a_normal (vec3)
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE,
|
||||
(GLsizei)(6 * sizeof(float)),
|
||||
(const void*)(3 * sizeof(float)));
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
|
||||
g.index_count = (int)mesh.indices.size();
|
||||
return g;
|
||||
}
|
||||
|
||||
void mesh_gpu_destroy(MeshGpu& g) {
|
||||
if (g.ebo) { glDeleteBuffers(1, &g.ebo); g.ebo = 0; }
|
||||
if (g.vbo) { glDeleteBuffers(1, &g.vbo); g.vbo = 0; }
|
||||
if (g.vao) { glDeleteVertexArrays(1, &g.vao); g.vao = 0; }
|
||||
g.index_count = 0;
|
||||
}
|
||||
|
||||
} // namespace fn::gfx
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "gfx/gl_loader.h"
|
||||
#include "gfx/mesh_obj_load.h"
|
||||
|
||||
namespace fn::gfx {
|
||||
|
||||
// VAO + VBO interleaved (pos.xyz, nrm.xyz) + EBO. ok() despues de upload.
|
||||
struct MeshGpu {
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
GLuint ebo = 0;
|
||||
int index_count = 0;
|
||||
bool ok() const { return vao != 0 && index_count > 0; }
|
||||
};
|
||||
|
||||
// Sube un Mesh CPU al GPU. Requiere contexto GL activo.
|
||||
// Si Mesh esta vacio o no es valido, devuelve MeshGpu{} (ok() == false).
|
||||
// Layout: location 0 = vec3 a_pos, location 1 = vec3 a_normal.
|
||||
MeshGpu mesh_gpu_upload(const Mesh& mesh);
|
||||
|
||||
// Libera los recursos GL. Seguro con mesh_gpu vacio.
|
||||
void mesh_gpu_destroy(MeshGpu& mesh_gpu);
|
||||
|
||||
} // namespace fn::gfx
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: mesh_gpu
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: gfx
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "MeshGpu mesh_gpu_upload(const Mesh&); void mesh_gpu_destroy(MeshGpu&)"
|
||||
description: "Sube un Mesh CPU a OpenGL como VAO + VBO interleaved (pos.xyz, normal.xyz) + EBO uint32. Layout: location 0 = a_pos vec3, location 1 = a_normal vec3, stride 6 floats."
|
||||
tags: [opengl, mesh, vao, vbo, ebo, gpu, gfx]
|
||||
uses_functions: [mesh_obj_load_cpp_gfx]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [GL/gl.h, GL/glext.h]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/gfx/mesh_gpu.cpp"
|
||||
framework: opengl
|
||||
params:
|
||||
- name: mesh
|
||||
desc: "Mesh CPU con positions/normals (mismo length, stride 3) e indices uint32. Si esta vacio o invalido, upload devuelve MeshGpu{} (ok()==false)."
|
||||
- name: mesh_gpu
|
||||
desc: "MeshGpu (vao/vbo/ebo, index_count). destroy libera todo y pone IDs a 0."
|
||||
output: "mesh_gpu_upload: MeshGpu listo para draw con glDrawElements(GL_TRIANGLES, index_count, GL_UNSIGNED_INT, 0). Si !ok(), no hubo upload."
|
||||
---
|
||||
|
||||
# mesh_gpu
|
||||
|
||||
CRUD GPU minimal para `Mesh`. Asume contexto OpenGL 3.3+ activo.
|
||||
|
||||
## Layout de attribs
|
||||
|
||||
```glsl
|
||||
#version 330 core
|
||||
layout(location = 0) in vec3 a_pos;
|
||||
layout(location = 1) in vec3 a_normal;
|
||||
```
|
||||
|
||||
Stride = `6 * sizeof(float)`, sin padding.
|
||||
|
||||
## Uso tipico
|
||||
|
||||
```cpp
|
||||
auto cpu = fn::gfx::mesh_obj_load("model.obj");
|
||||
auto gpu = fn::gfx::mesh_gpu_upload(cpu);
|
||||
if (!gpu.ok()) { /* falla */ return; }
|
||||
|
||||
// Draw:
|
||||
glUseProgram(prog);
|
||||
glBindVertexArray(gpu.vao);
|
||||
glDrawElements(GL_TRIANGLES, gpu.index_count, GL_UNSIGNED_INT, 0);
|
||||
glBindVertexArray(0);
|
||||
|
||||
// Cleanup:
|
||||
fn::gfx::mesh_gpu_destroy(gpu);
|
||||
```
|
||||
|
||||
## Validacion de input
|
||||
|
||||
`mesh_gpu_upload` exige que `mesh.normals.size() == mesh.positions.size()`. `mesh_obj_parse` siempre genera normales (per-face si faltan) → invariante natural.
|
||||
|
||||
## Notas
|
||||
|
||||
- Indices son `GL_UNSIGNED_INT` (32-bit) para soportar meshes grandes sin tener que decidir formato dinamicamente.
|
||||
- `GL_STATIC_DRAW`: el assumption es que la malla no cambia post-upload. Si necesitas streaming, crear otro helper con `GL_DYNAMIC_DRAW`.
|
||||
@@ -0,0 +1,241 @@
|
||||
#include "gfx/mesh_obj_load.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace fn::gfx {
|
||||
|
||||
namespace {
|
||||
|
||||
// Parse one face vertex token "v", "v/t", "v//n", or "v/t/n".
|
||||
// Returns 0-based positive index for v_idx and n_idx (or -1 if not present).
|
||||
// .obj indices are 1-based; negative indices are relative-to-end.
|
||||
struct FaceVert { int v = -1; int n = -1; };
|
||||
|
||||
FaceVert parse_face_vert(const char* tok, int n_pos, int n_nrm) {
|
||||
FaceVert r;
|
||||
int v = 0, t = 0, n = 0;
|
||||
int slashes = 0;
|
||||
const char* p = tok;
|
||||
// crude split by '/'
|
||||
char buf[3][32];
|
||||
int bi = 0;
|
||||
int bj = 0;
|
||||
buf[0][0] = buf[1][0] = buf[2][0] = '\0';
|
||||
while (*p && *p != '\0' && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n') {
|
||||
if (*p == '/') { buf[bi][bj] = '\0'; bi++; bj = 0; if (bi > 2) break; slashes++; }
|
||||
else if (bj < 31) { buf[bi][bj++] = *p; }
|
||||
p++;
|
||||
}
|
||||
if (bi <= 2) buf[bi][bj] = '\0';
|
||||
|
||||
if (buf[0][0]) v = std::atoi(buf[0]);
|
||||
if (slashes >= 1 && buf[1][0]) t = std::atoi(buf[1]);
|
||||
if (slashes == 2 && buf[2][0]) n = std::atoi(buf[2]);
|
||||
(void)t;
|
||||
|
||||
// resolve 1-based / negative
|
||||
if (v > 0) r.v = v - 1;
|
||||
else if (v < 0) r.v = n_pos + v;
|
||||
if (n > 0) r.n = n - 1;
|
||||
else if (n < 0) r.n = n_nrm + n;
|
||||
return r;
|
||||
}
|
||||
|
||||
void cross3(const float a[3], const float b[3], float out[3]) {
|
||||
out[0] = a[1]*b[2] - a[2]*b[1];
|
||||
out[1] = a[2]*b[0] - a[0]*b[2];
|
||||
out[2] = a[0]*b[1] - a[1]*b[0];
|
||||
}
|
||||
void sub3(const float a[3], const float b[3], float out[3]) {
|
||||
out[0] = a[0]-b[0]; out[1] = a[1]-b[1]; out[2] = a[2]-b[2];
|
||||
}
|
||||
void normalize3(float v[3]) {
|
||||
float l = std::sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
|
||||
if (l > 0) { v[0] /= l; v[1] /= l; v[2] /= l; }
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Mesh mesh_obj_parse(const char* obj_text, size_t len) {
|
||||
Mesh m;
|
||||
if (!obj_text || len == 0) return m;
|
||||
|
||||
// Raw arrays from the file.
|
||||
std::vector<float> raw_pos; // stride 3
|
||||
std::vector<float> raw_nrm; // stride 3
|
||||
|
||||
// Per-output-vertex: dedup by (v_idx, n_idx) tuple. Map "v_n" -> out index.
|
||||
// For tiny meshes a linear scan of pairs suffices, but we use a vector +
|
||||
// simple hash map via std::string key (KISS, ~MB-scale meshes still ok).
|
||||
struct Key { int v; int n; };
|
||||
std::vector<Key> out_keys;
|
||||
|
||||
auto find_or_add = [&](int v_idx, int n_idx) -> uint32_t {
|
||||
// linear scan — fine for inspection meshes (<<100k unique verts).
|
||||
for (uint32_t i = 0; i < out_keys.size(); ++i) {
|
||||
if (out_keys[i].v == v_idx && out_keys[i].n == n_idx) return i;
|
||||
}
|
||||
out_keys.push_back({v_idx, n_idx});
|
||||
return (uint32_t)(out_keys.size() - 1);
|
||||
};
|
||||
|
||||
// Iterate lines.
|
||||
const char* p = obj_text;
|
||||
const char* end = obj_text + len;
|
||||
|
||||
// Stash face vertices for second pass (if normals are missing globally
|
||||
// we generate per face).
|
||||
struct FaceTri { FaceVert v[3]; };
|
||||
std::vector<FaceTri> faces;
|
||||
|
||||
auto skip_ws = [](const char*& q, const char* qend) {
|
||||
while (q < qend && (*q == ' ' || *q == '\t')) q++;
|
||||
};
|
||||
|
||||
while (p < end) {
|
||||
// skip leading whitespace
|
||||
skip_ws(p, end);
|
||||
if (p >= end) break;
|
||||
if (*p == '#') {
|
||||
while (p < end && *p != '\n') p++;
|
||||
if (p < end) p++;
|
||||
continue;
|
||||
}
|
||||
if (*p == '\n' || *p == '\r') { p++; continue; }
|
||||
|
||||
// tag
|
||||
const char* tag_start = p;
|
||||
while (p < end && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++;
|
||||
size_t tag_len = (size_t)(p - tag_start);
|
||||
|
||||
auto eat_line = [&](std::string& out) {
|
||||
skip_ws(p, end);
|
||||
const char* line_start = p;
|
||||
while (p < end && *p != '\n' && *p != '\r') p++;
|
||||
out.assign(line_start, p);
|
||||
while (p < end && (*p == '\n' || *p == '\r')) p++;
|
||||
};
|
||||
|
||||
if (tag_len == 1 && tag_start[0] == 'v') {
|
||||
std::string body; eat_line(body);
|
||||
float x = 0, y = 0, z = 0;
|
||||
std::sscanf(body.c_str(), "%f %f %f", &x, &y, &z);
|
||||
raw_pos.push_back(x); raw_pos.push_back(y); raw_pos.push_back(z);
|
||||
} else if (tag_len == 2 && tag_start[0] == 'v' && tag_start[1] == 'n') {
|
||||
std::string body; eat_line(body);
|
||||
float x = 0, y = 0, z = 0;
|
||||
std::sscanf(body.c_str(), "%f %f %f", &x, &y, &z);
|
||||
raw_nrm.push_back(x); raw_nrm.push_back(y); raw_nrm.push_back(z);
|
||||
} else if (tag_len == 1 && tag_start[0] == 'f') {
|
||||
std::string body; eat_line(body);
|
||||
// Split body into space-delimited tokens.
|
||||
std::vector<FaceVert> fv;
|
||||
const char* bp = body.c_str();
|
||||
const char* be = bp + body.size();
|
||||
int npos = (int)(raw_pos.size() / 3);
|
||||
int nnrm = (int)(raw_nrm.size() / 3);
|
||||
while (bp < be) {
|
||||
while (bp < be && (*bp == ' ' || *bp == '\t')) bp++;
|
||||
if (bp >= be) break;
|
||||
fv.push_back(parse_face_vert(bp, npos, nnrm));
|
||||
while (bp < be && *bp != ' ' && *bp != '\t') bp++;
|
||||
}
|
||||
if (fv.size() == 3) {
|
||||
faces.push_back({fv[0], fv[1], fv[2]});
|
||||
} else if (fv.size() == 4) {
|
||||
faces.push_back({fv[0], fv[1], fv[2]});
|
||||
faces.push_back({fv[0], fv[2], fv[3]});
|
||||
}
|
||||
// n-gons silently dropped; documented in .md
|
||||
} else {
|
||||
// unknown tag — skip to EOL
|
||||
while (p < end && *p != '\n') p++;
|
||||
if (p < end) p++;
|
||||
}
|
||||
}
|
||||
|
||||
if (raw_pos.empty() || faces.empty()) return Mesh{};
|
||||
|
||||
// Build output vertices/normals/indices.
|
||||
// If any face vertex lacks a normal, we'll generate face normals.
|
||||
bool need_face_normals = raw_nrm.empty();
|
||||
if (!need_face_normals) {
|
||||
for (auto& f : faces) {
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
if (f.v[k].n < 0) { need_face_normals = true; break; }
|
||||
}
|
||||
if (need_face_normals) break;
|
||||
}
|
||||
}
|
||||
|
||||
// First pass: build deduped vertex list with explicit normals (when present).
|
||||
// If we need face normals, we'll emit unique vertices per face (no dedup
|
||||
// across faces for normals; positions can still be shared but we just
|
||||
// duplicate to keep code simple — this is flat shading by design).
|
||||
if (need_face_normals) {
|
||||
m.positions.reserve(faces.size() * 3 * 3);
|
||||
m.normals.reserve(faces.size() * 3 * 3);
|
||||
m.indices.reserve(faces.size() * 3);
|
||||
uint32_t next = 0;
|
||||
for (auto& f : faces) {
|
||||
const float* p0 = &raw_pos[f.v[0].v * 3];
|
||||
const float* p1 = &raw_pos[f.v[1].v * 3];
|
||||
const float* p2 = &raw_pos[f.v[2].v * 3];
|
||||
float e1[3], e2[3], n[3];
|
||||
sub3(p1, p0, e1);
|
||||
sub3(p2, p0, e2);
|
||||
cross3(e1, e2, n);
|
||||
normalize3(n);
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const float* pp = &raw_pos[f.v[k].v * 3];
|
||||
m.positions.push_back(pp[0]);
|
||||
m.positions.push_back(pp[1]);
|
||||
m.positions.push_back(pp[2]);
|
||||
m.normals.push_back(n[0]);
|
||||
m.normals.push_back(n[1]);
|
||||
m.normals.push_back(n[2]);
|
||||
m.indices.push_back(next++);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m.indices.reserve(faces.size() * 3);
|
||||
for (auto& f : faces) {
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
uint32_t out = find_or_add(f.v[k].v, f.v[k].n);
|
||||
m.indices.push_back(out);
|
||||
}
|
||||
}
|
||||
m.positions.resize(out_keys.size() * 3);
|
||||
m.normals.resize(out_keys.size() * 3);
|
||||
for (size_t i = 0; i < out_keys.size(); ++i) {
|
||||
int vi = out_keys[i].v;
|
||||
int ni = out_keys[i].n;
|
||||
m.positions[i*3 + 0] = raw_pos[vi*3 + 0];
|
||||
m.positions[i*3 + 1] = raw_pos[vi*3 + 1];
|
||||
m.positions[i*3 + 2] = raw_pos[vi*3 + 2];
|
||||
m.normals[i*3 + 0] = raw_nrm[ni*3 + 0];
|
||||
m.normals[i*3 + 1] = raw_nrm[ni*3 + 1];
|
||||
m.normals[i*3 + 2] = raw_nrm[ni*3 + 2];
|
||||
}
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
Mesh mesh_obj_load(const char* path) {
|
||||
if (!path || !*path) return Mesh{};
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f) return Mesh{};
|
||||
std::ostringstream ss;
|
||||
ss << f.rdbuf();
|
||||
std::string s = ss.str();
|
||||
return mesh_obj_parse(s.data(), s.size());
|
||||
}
|
||||
|
||||
} // namespace fn::gfx
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace fn::gfx {
|
||||
|
||||
// CPU-side mesh: positions interleaved with normals stride 3 floats each,
|
||||
// indexed by `indices`.
|
||||
struct Mesh {
|
||||
std::vector<float> positions; // x,y,z stride=3 (count = num_vertices*3)
|
||||
std::vector<float> normals; // x,y,z stride=3 (same length as positions)
|
||||
std::vector<uint32_t> indices; // tri-list (count multiple of 3)
|
||||
};
|
||||
|
||||
// Parse Wavefront .obj from a text buffer. Pure: no I/O.
|
||||
// Supports: v, vn, f (triangles and quads). Ignores: vt, mtllib, usemtl, o, g, s.
|
||||
// If the file has no vn, normals are generated per-face (flat shading).
|
||||
// Quads in `f` are split into 2 triangles (fan). N-gons (>4 verts) are not supported.
|
||||
//
|
||||
// Returns an empty Mesh ({}, {}, {}) on parse failure (no vertices found).
|
||||
Mesh mesh_obj_parse(const char* obj_text, size_t len);
|
||||
|
||||
// Read file from disk and parse. IMPURE (file I/O).
|
||||
// Returns empty Mesh on read or parse failure.
|
||||
Mesh mesh_obj_load(const char* path);
|
||||
|
||||
} // namespace fn::gfx
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
name: mesh_obj_load
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: gfx
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "Mesh mesh_obj_parse(const char* obj_text, size_t len); Mesh mesh_obj_load(const char* path)"
|
||||
description: "Parser minimal de Wavefront .obj — soporta v, vn, f (tris y quads). Genera normales por face si faltan. mesh_obj_parse es puro; mesh_obj_load es helper impuro que lee fichero y delega."
|
||||
tags: [obj, mesh, parser, wavefront, loader, geometry, 3d]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: [fstream, sstream, cmath]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/gfx/mesh_obj_load.cpp"
|
||||
framework: opengl
|
||||
params:
|
||||
- name: obj_text
|
||||
desc: "Buffer de texto .obj. Lineas reconocidas: v, vn, f. Comentarios con #. UTF-8 plano."
|
||||
- name: len
|
||||
desc: "Longitud del buffer en bytes"
|
||||
- name: path
|
||||
desc: "Ruta absoluta o relativa del .obj a leer (mesh_obj_load impuro)"
|
||||
output: "Mesh con positions/normals (stride 3, mismo length) y indices (tri-list, multiplo de 3). Si no hay vn, normales por face (flat shading) y vertices duplicados por face. Mesh vacio si parse falla."
|
||||
---
|
||||
|
||||
# mesh_obj_load
|
||||
|
||||
Parser pequeno de Wavefront `.obj` para inspeccion de geometria. KISS: cubre el subconjunto que la mayoria de exporters genera (Blender default, MeshLab, etc).
|
||||
|
||||
## Soporta
|
||||
|
||||
- `v x y z` — vertice (xyz; w opcional ignorado)
|
||||
- `vn x y z` — normal
|
||||
- `f a b c` y `f a b c d` — tris y quads (los quads se dividen en 2 tris)
|
||||
- Indices `v`, `v/t`, `v//n`, `v/t/n` (vt se ignora; t no afecta)
|
||||
- Indices 1-based positivos y negativos (relative-to-end, conforme spec)
|
||||
- Lineas en blanco y comentarios con `#`
|
||||
|
||||
## NO soporta (en este issue)
|
||||
|
||||
- N-gons con mas de 4 vertices → silenciosamente descartados
|
||||
- `vt` (coordenadas de textura) — el indice se parsea pero el dato se ignora
|
||||
- `mtllib`, `usemtl`, `o`, `g`, `s` — se saltan
|
||||
- Lineas de polilineas (`l`)
|
||||
- Curvas, superficies, `vp`
|
||||
|
||||
## Generacion de normales
|
||||
|
||||
Si el `.obj` no tiene `vn` (o si alguna face no las referencia), se generan normales por face (flat shading). Esto duplica vertices entre faces que comparten posicion pero no normal — es el comportamiento esperado para inspeccion.
|
||||
|
||||
Cuando todas las faces tienen normales explicitas, se hace dedup `(v_idx, n_idx)` con un linear-scan simple. Para meshes inspeccion (<<100k verts unicos) es perf-OK; meshes mas grandes se beneficiarian de un hashmap.
|
||||
|
||||
## Errores
|
||||
|
||||
Si el archivo no tiene vertices o no tiene faces, devuelve `Mesh{}` (positions/indices vacios). El caller puede chequear con `mesh.positions.empty()`.
|
||||
|
||||
## Test inline (cubo)
|
||||
|
||||
```cpp
|
||||
const char* obj =
|
||||
"v -1 -1 -1\nv 1 -1 -1\nv 1 1 -1\nv -1 1 -1\n"
|
||||
"v -1 -1 1\nv 1 -1 1\nv 1 1 1\nv -1 1 1\n"
|
||||
"f 1 2 3 4\nf 5 6 7 8\nf 1 2 6 5\n"
|
||||
"f 4 3 7 8\nf 1 4 8 5\nf 2 3 7 6\n";
|
||||
Mesh m = mesh_obj_parse(obj, std::strlen(obj));
|
||||
// 6 quads -> 12 tris -> 36 indices.
|
||||
// Sin vn -> face normals -> 36 verts emitidos (duplicados por face).
|
||||
assert(m.indices.size() == 36);
|
||||
```
|
||||
@@ -0,0 +1,207 @@
|
||||
#include "viz/mesh_viewer.h"
|
||||
|
||||
#include "gfx/gl_loader.h"
|
||||
#include "gfx/gl_framebuffer.h"
|
||||
#include "gfx/gl_shader.h"
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace fn::viz {
|
||||
|
||||
namespace {
|
||||
|
||||
// Vertex+fragment shader with Lambert headlight (light = camera direction).
|
||||
// We bypass gl_shader::compile_fragment because that helper assumes a
|
||||
// fullscreen-quad pipeline; here we need explicit attribs and a vertex shader
|
||||
// that consumes mvp + normalMatrix.
|
||||
|
||||
const char* kVert = R"glsl(
|
||||
#version 330 core
|
||||
layout(location = 0) in vec3 a_pos;
|
||||
layout(location = 1) in vec3 a_normal;
|
||||
|
||||
uniform mat4 u_view;
|
||||
uniform mat4 u_proj;
|
||||
|
||||
out vec3 v_normal_view;
|
||||
|
||||
void main() {
|
||||
vec4 view_pos = u_view * vec4(a_pos, 1.0);
|
||||
// For pure rotation in the view part of orbit-camera, the upper-left 3x3
|
||||
// suffices as the normal matrix.
|
||||
v_normal_view = mat3(u_view) * a_normal;
|
||||
gl_Position = u_proj * view_pos;
|
||||
}
|
||||
)glsl";
|
||||
|
||||
const char* kFrag = R"glsl(
|
||||
#version 330 core
|
||||
in vec3 v_normal_view;
|
||||
out vec4 frag;
|
||||
|
||||
uniform vec4 u_color;
|
||||
|
||||
void main() {
|
||||
// Headlight: light dir = +Z in view space (toward camera in right-handed view).
|
||||
vec3 N = normalize(v_normal_view);
|
||||
vec3 L = vec3(0.0, 0.0, 1.0);
|
||||
float ndl = max(dot(N, L), 0.0);
|
||||
// ambient 0.2 + diffuse 0.8
|
||||
float lighting = 0.2 + 0.8 * ndl;
|
||||
frag = vec4(u_color.rgb * lighting, u_color.a);
|
||||
}
|
||||
)glsl";
|
||||
|
||||
// Per-id cached state.
|
||||
struct Cache {
|
||||
fn::gfx::Framebuffer fb{};
|
||||
GLuint program = 0;
|
||||
GLint loc_view = -1;
|
||||
GLint loc_proj = -1;
|
||||
GLint loc_color = -1;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, Cache>& caches() {
|
||||
static std::unordered_map<std::string, Cache> m;
|
||||
return m;
|
||||
}
|
||||
|
||||
GLuint compile_program() {
|
||||
// We need vertex+fragment, but gl_shader::compile_fragment only does
|
||||
// fragment with a fixed vertex. Inline a small compile here.
|
||||
auto compile = [](GLenum type, const char* src) -> GLuint {
|
||||
GLuint sh = glCreateShader(type);
|
||||
glShaderSource(sh, 1, &src, nullptr);
|
||||
glCompileShader(sh);
|
||||
GLint ok = 0;
|
||||
glGetShaderiv(sh, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char log[512];
|
||||
glGetShaderInfoLog(sh, sizeof(log), nullptr, log);
|
||||
(void)log; // could log via ImGui
|
||||
glDeleteShader(sh);
|
||||
return 0;
|
||||
}
|
||||
return sh;
|
||||
};
|
||||
GLuint v = compile(GL_VERTEX_SHADER, kVert);
|
||||
if (!v) return 0;
|
||||
GLuint f = compile(GL_FRAGMENT_SHADER, kFrag);
|
||||
if (!f) { glDeleteShader(v); return 0; }
|
||||
GLuint p = glCreateProgram();
|
||||
glAttachShader(p, v);
|
||||
glAttachShader(p, f);
|
||||
glLinkProgram(p);
|
||||
glDeleteShader(v);
|
||||
glDeleteShader(f);
|
||||
GLint ok = 0;
|
||||
glGetProgramiv(p, GL_LINK_STATUS, &ok);
|
||||
if (!ok) { glDeleteProgram(p); return 0; }
|
||||
return p;
|
||||
}
|
||||
|
||||
void ensure_init(Cache& c) {
|
||||
if (c.initialized) return;
|
||||
fn::gfx::gl_loader_init();
|
||||
fn::gfx::fb_init(c.fb);
|
||||
c.program = compile_program();
|
||||
if (c.program) {
|
||||
c.loc_view = glGetUniformLocation(c.program, "u_view");
|
||||
c.loc_proj = glGetUniformLocation(c.program, "u_proj");
|
||||
c.loc_color = glGetUniformLocation(c.program, "u_color");
|
||||
}
|
||||
c.initialized = true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void mesh_viewer(const char* id, const MeshViewerConfig& cfg) {
|
||||
if (!id) id = "##mesh_viewer";
|
||||
|
||||
// Resolve panel size.
|
||||
ImVec2 size = cfg.size;
|
||||
if (size.x <= 0) size.x = ImGui::GetContentRegionAvail().x;
|
||||
if (size.y <= 0) size.y = 400.0f;
|
||||
int w = (int)size.x; if (w < 1) w = 1;
|
||||
int h = (int)size.y; if (h < 1) h = 1;
|
||||
|
||||
auto& c = caches()[id];
|
||||
ensure_init(c);
|
||||
|
||||
if (cfg.mesh && cfg.mesh->ok() && cfg.cam && c.program) {
|
||||
cfg.cam->aspect = (float)w / (float)h;
|
||||
|
||||
fn::gfx::fb_resize(c.fb, w, h);
|
||||
|
||||
// Save GL state.
|
||||
GLint prev_fbo = 0;
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo);
|
||||
GLint prev_vp[4];
|
||||
glGetIntegerv(GL_VIEWPORT, prev_vp);
|
||||
GLboolean prev_depth = glIsEnabled(GL_DEPTH_TEST);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, c.fb.fbo);
|
||||
glViewport(0, 0, w, h);
|
||||
glClearColor(0.10f, 0.10f, 0.13f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
// No depth attachment in our FBO — fall back to back-to-front-ish via
|
||||
// GL_DEPTH_TEST off. For inspection meshes this is fine; documented.
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
|
||||
glUseProgram(c.program);
|
||||
auto m = fn::core::orbit_camera_matrices(*cfg.cam);
|
||||
// Row-major -> upload with transpose=GL_TRUE.
|
||||
if (c.loc_view >= 0) glUniformMatrix4fv(c.loc_view, 1, GL_TRUE, m.view);
|
||||
if (c.loc_proj >= 0) glUniformMatrix4fv(c.loc_proj, 1, GL_TRUE, m.proj);
|
||||
if (c.loc_color >= 0) {
|
||||
float r = ((cfg.color >> 0) & 0xFF) / 255.0f;
|
||||
float g = ((cfg.color >> 8) & 0xFF) / 255.0f;
|
||||
float b = ((cfg.color >> 16) & 0xFF) / 255.0f;
|
||||
float a = ((cfg.color >> 24) & 0xFF) / 255.0f;
|
||||
glUniform4f(c.loc_color, r, g, b, a);
|
||||
}
|
||||
|
||||
#ifndef __EMSCRIPTEN__
|
||||
if (cfg.wireframe) {
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
|
||||
} else {
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||
}
|
||||
#endif
|
||||
|
||||
glBindVertexArray(cfg.mesh->vao);
|
||||
glDrawElements(GL_TRIANGLES, cfg.mesh->index_count, GL_UNSIGNED_INT, 0);
|
||||
glBindVertexArray(0);
|
||||
|
||||
#ifndef __EMSCRIPTEN__
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||
#endif
|
||||
glUseProgram(0);
|
||||
|
||||
// Restore GL state.
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, (GLuint)prev_fbo);
|
||||
glViewport(prev_vp[0], prev_vp[1], prev_vp[2], prev_vp[3]);
|
||||
if (prev_depth) glEnable(GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
// Display.
|
||||
ImGui::Image((ImTextureID)(intptr_t)c.fb.tex,
|
||||
ImVec2((float)w, (float)h),
|
||||
ImVec2(0, 1), ImVec2(1, 0));
|
||||
|
||||
if (ImGui::IsItemActive() && cfg.cam) {
|
||||
ImVec2 d = ImGui::GetIO().MouseDelta;
|
||||
fn::core::orbit_camera_handle_drag(*cfg.cam, d, 0.0f);
|
||||
}
|
||||
if (ImGui::IsItemHovered() && cfg.cam) {
|
||||
float w_scroll = ImGui::GetIO().MouseWheel;
|
||||
if (w_scroll != 0.0f) {
|
||||
ImVec2 zero{0, 0};
|
||||
fn::core::orbit_camera_handle_drag(*cfg.cam, zero, w_scroll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fn::viz
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/orbit_camera.h"
|
||||
#include "gfx/mesh_gpu.h"
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
namespace fn::viz {
|
||||
|
||||
struct MeshViewerConfig {
|
||||
const fn::gfx::MeshGpu* mesh = nullptr;
|
||||
fn::core::OrbitCamera* cam = nullptr; // mutable: drag/wheel handler
|
||||
ImVec2 size = {-1.0f, 400.0f}; // -1 = stretch X
|
||||
ImU32 color = IM_COL32(180, 180, 200, 255);
|
||||
bool wireframe = false;
|
||||
};
|
||||
|
||||
// Renderiza un MeshGpu en un FBO interno (cacheado por id) y muestra
|
||||
// la textura via ImGui::Image. Maneja drag/wheel sobre el panel.
|
||||
//
|
||||
// El `id` debe ser estable para reusar el mismo FBO entre frames.
|
||||
void mesh_viewer(const char* id, const MeshViewerConfig& cfg);
|
||||
|
||||
} // namespace fn::viz
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: mesh_viewer
|
||||
kind: component
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "void mesh_viewer(const char* id, const MeshViewerConfig& cfg)"
|
||||
description: "Renderiza un MeshGpu (3D) en un FBO interno cacheado por id, con orbit camera, iluminacion Lambert headlight, opcion wireframe. Drag/wheel del mouse mueven la camara."
|
||||
tags: [imgui, opengl, mesh, 3d, viewer, viz, fbo]
|
||||
uses_functions: [mesh_gpu_cpp_gfx, orbit_camera_cpp_core, gl_framebuffer_cpp_gfx]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [imgui, GL/gl.h]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/viz/mesh_viewer.cpp"
|
||||
framework: imgui
|
||||
emits: ["camera_drag", "camera_zoom"]
|
||||
output: "Renderiza una imagen del mesh dentro del frame ImGui actual; muta cfg.cam in-place segun drag/wheel del mouse cuando el panel esta active/hovered."
|
||||
---
|
||||
|
||||
# mesh_viewer
|
||||
|
||||
Componente de viz para inspeccionar geometria 3D dentro de cualquier panel ImGui. Internamente:
|
||||
|
||||
1. Compila/cachea (por `id`) un programa shader Lambert headlight (vertex + fragment).
|
||||
2. Cachea un `Framebuffer` por `id` y lo redimensiona segun `cfg.size`.
|
||||
3. Cada frame: bind FBO, draw `cfg.mesh`, mostrar la textura via `ImGui::Image`.
|
||||
4. Si el panel esta active → llama `orbit_camera_handle_drag` con `MouseDelta`.
|
||||
5. Si el panel esta hovered y hay scroll → ajusta zoom.
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```cpp
|
||||
static fn::core::OrbitCamera cam;
|
||||
fn::viz::MeshViewerConfig cfg{};
|
||||
cfg.mesh = &gpu; // MeshGpu valido
|
||||
cfg.cam = &cam;
|
||||
cfg.size = {-1, 480};
|
||||
cfg.wireframe = false;
|
||||
cfg.color = IM_COL32(160, 200, 255, 255);
|
||||
fn::viz::mesh_viewer("##teapot_view", cfg);
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- **Sin depth buffer**: el FBO solo tiene attachment color (sigue el patron de `gl_framebuffer`). Para meshes complejos con auto-oclusion, esto produce artefactos. Issue futuro puede añadir depth/stencil renderbuffer.
|
||||
- **Wireframe**: usa `glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)` (no disponible en GL ES; protegido con `#ifndef __EMSCRIPTEN__`).
|
||||
- **Cache por id**: si el `id` cambia dinamicamente entre frames, se acumulan FBOs y programas en memoria (leak). Usar IDs estables.
|
||||
- **Iluminacion**: Lambert con luz fija en `+Z` view-space ("headlight"), suficiente para inspeccion. Sin specular, sin sombras.
|
||||
- **Matrices**: row-major desde `orbit_camera_matrices`; se suben con `transpose=GL_TRUE` (GL espera column-major).
|
||||
Reference in New Issue
Block a user