diff --git a/cpp/functions/gfx/gl_texture_load.cpp b/cpp/functions/gfx/gl_texture_load.cpp new file mode 100644 index 00000000..25d709dc --- /dev/null +++ b/cpp/functions/gfx/gl_texture_load.cpp @@ -0,0 +1,151 @@ +#include "gl_texture_load.h" + +#include "../../vendor/stb/stb_image.h" + +#include +#include + +namespace fn { + +namespace { + +// Error thread-local — cada thread mantiene su propio mensaje. +thread_local std::string g_last_error; + +bool ends_with_ci(const char* s, const char* suffix) { + if (!s || !suffix) return false; + size_t ls = std::strlen(s); + size_t lf = std::strlen(suffix); + if (lf > ls) return false; + const char* a = s + ls - lf; + for (size_t i = 0; i < lf; i++) { + char ca = a[i]; char cb = suffix[i]; + if (ca >= 'A' && ca <= 'Z') ca = char(ca - 'A' + 'a'); + if (cb >= 'A' && cb <= 'Z') cb = char(cb - 'A' + 'a'); + if (ca != cb) return false; + } + return true; +} + +GLuint upload_ldr(const unsigned char* pixels, int w, int h, bool srgb) { + GLuint id = 0; + glGenTextures(1, &id); + if (id == 0) return 0; + glBindTexture(GL_TEXTURE_2D, id); + GLint internal = srgb ? GL_SRGB8_ALPHA8 : GL_RGBA8; + glTexImage2D(GL_TEXTURE_2D, 0, internal, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glGenerateMipmap(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, 0); + return id; +} + +GLuint upload_hdr(const float* pixels, int w, int h) { + GLuint id = 0; + glGenTextures(1, &id); + if (id == 0) return 0; + glBindTexture(GL_TEXTURE_2D, id); + // GL_RGBA16F + GL_FLOAT — GL 3.0+. En GL 2.1 no hay GL_RGBA16F; aceptamos + // la limitacion (driver moderno). + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, w, h, 0, GL_RGBA, GL_FLOAT, pixels); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glGenerateMipmap(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, 0); + return id; +} + +void set_error(const char* msg) { + g_last_error = msg ? msg : "unknown error"; +} + +} // namespace + +GlTexture gl_texture_load(const char* path, bool flip_y, bool srgb) { + GlTexture tex{}; + if (!path || !*path) { + set_error("gl_texture_load: empty path"); + return tex; + } + + stbi_set_flip_vertically_on_load(flip_y ? 1 : 0); + + if (ends_with_ci(path, ".hdr")) { + int w = 0, h = 0, ch = 0; + float* px = stbi_loadf(path, &w, &h, &ch, 4); + if (!px) { + set_error(stbi_failure_reason()); + return tex; + } + tex.id = upload_hdr(px, w, h); + tex.w = w; tex.h = h; tex.channels = ch; + stbi_image_free(px); + if (tex.id == 0) set_error("glGenTextures returned 0"); + else g_last_error.clear(); + return tex; + } + + int w = 0, h = 0, ch = 0; + unsigned char* px = stbi_load(path, &w, &h, &ch, 4); // forzamos RGBA + if (!px) { + set_error(stbi_failure_reason()); + return tex; + } + tex.id = upload_ldr(px, w, h, srgb); + tex.w = w; tex.h = h; tex.channels = ch; + stbi_image_free(px); + if (tex.id == 0) set_error("glGenTextures returned 0"); + else g_last_error.clear(); + return tex; +} + +GlTexture gl_texture_load_from_memory(const unsigned char* data, int size, + bool flip_y, bool srgb) { + GlTexture tex{}; + if (!data || size <= 0) { + set_error("gl_texture_load_from_memory: empty buffer"); + return tex; + } + stbi_set_flip_vertically_on_load(flip_y ? 1 : 0); + + int w = 0, h = 0, ch = 0; + unsigned char* px = stbi_load_from_memory(data, size, &w, &h, &ch, 4); + if (!px) { + set_error(stbi_failure_reason()); + return tex; + } + tex.id = upload_ldr(px, w, h, srgb); + tex.w = w; tex.h = h; tex.channels = ch; + stbi_image_free(px); + if (tex.id == 0) set_error("glGenTextures returned 0"); + else g_last_error.clear(); + return tex; +} + +void gl_texture_destroy(GlTexture& tex) { + if (tex.id != 0) { + glDeleteTextures(1, &tex.id); + tex.id = 0; + } + tex.w = tex.h = tex.channels = 0; +} + +const char* gl_texture_last_error() { + return g_last_error.c_str(); +} + +void gl_texture_bind_uniform(GLuint program, const char* name, + const GlTexture& tex, int unit) { + if (program == 0 || !name || !tex.ok()) return; + glActiveTexture(GL_TEXTURE0 + unit); + glBindTexture(GL_TEXTURE_2D, tex.id); + GLint loc = glGetUniformLocation(program, name); + if (loc >= 0) glUniform1i(loc, unit); +} + +} // namespace fn diff --git a/cpp/functions/gfx/gl_texture_load.h b/cpp/functions/gfx/gl_texture_load.h new file mode 100644 index 00000000..1f49219f --- /dev/null +++ b/cpp/functions/gfx/gl_texture_load.h @@ -0,0 +1,44 @@ +#pragma once +// gl_texture_load — carga PNG/JPG/BMP/TGA/HDR desde disco o memoria a una +// textura OpenGL lista para usar como sampler2D en shaders. Vendorea stb_image +// en cpp/vendor/stb/. Funcion impura: hace I/O y crea recursos GPU. + +#include "gl_loader.h" + +namespace fn { + +struct GlTexture { + GLuint id = 0; + int w = 0; + int h = 0; + int channels = 0; + bool ok() const { return id != 0; } +}; + +// Carga desde disco. flip_y=true (por defecto) coincide con la convencion +// de OpenGL (V hacia arriba). srgb=true sube como GL_SRGB8_ALPHA8 (gamma +// correcto sin pow(c, 2.2) en shader). Si el path termina en .hdr se carga +// como float (GL_RGBA16F). +// +// Si falla, devuelve GlTexture con id=0 (ok() == false). Llama a +// gl_texture_last_error() para obtener el detalle. +GlTexture gl_texture_load(const char* path, bool flip_y = true, bool srgb = false); + +// Igual que arriba pero leyendo de un buffer en memoria (PNG/JPG/etc bytes). +GlTexture gl_texture_load_from_memory(const unsigned char* data, int size, + bool flip_y = true, bool srgb = false); + +// Libera la textura GPU y deja id=0. +void gl_texture_destroy(GlTexture& tex); + +// Mensaje de error del ultimo gl_texture_load* en este thread. "" si no hubo +// fallo. Permanece valido hasta el siguiente gl_texture_load* en el thread. +const char* gl_texture_last_error(); + +// Helper: bind a una texture unit y subir uniform sampler2D al programa. +// program debe ser un GLuint valido, name el nombre del uniform sampler2D, +// unit el slot de textura (0..N). +void gl_texture_bind_uniform(GLuint program, const char* name, + const GlTexture& tex, int unit); + +} // namespace fn diff --git a/cpp/functions/gfx/gl_texture_load.md b/cpp/functions/gfx/gl_texture_load.md new file mode 100644 index 00000000..e6dd6135 --- /dev/null +++ b/cpp/functions/gfx/gl_texture_load.md @@ -0,0 +1,108 @@ +--- +name: gl_texture_load +kind: function +lang: cpp +domain: gfx +version: "1.0.0" +purity: impure +signature: "GlTexture gl_texture_load(const char* path, bool flip_y, bool srgb)" +description: "Carga PNG/JPG/BMP/TGA/HDR desde disco (o memoria) a una textura OpenGL lista para usar como sampler2D. Vendorea stb_image. Soporta sRGB (GL_SRGB8_ALPHA8) y HDR (GL_RGBA16F via stbi_loadf). Genera mipmaps automaticamente. flip_y=true por defecto coincide con la convencion UV de OpenGL." +tags: [opengl, texture, image, png, jpg, hdr, stb_image, gfx] +uses_functions: [gl_loader_cpp_gfx] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [stb_image.h, GL/gl.h, GL/glext.h] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/gfx/gl_texture_load.cpp" +framework: opengl +params: + - name: path + desc: "Ruta al archivo de imagen (PNG/JPG/BMP/TGA o .hdr para float). UTF-8." + - name: flip_y + desc: "Si es true, voltea verticalmente al cargar (convencion OpenGL: V hacia arriba). Default true." + - name: srgb + desc: "Si es true, sube como GL_SRGB8_ALPHA8 (gamma correcto sin pow(c, 2.2) en shader). Solo aplica a LDR. Default false." +output: "GlTexture con id=GLuint listo para glBindTexture, w/h/channels de la imagen original. Si falla, id=0 y ok()==false; usar gl_texture_last_error() para detalle." +--- + +# gl_texture_load + +Funcion impura del registry C++ que carga una imagen desde disco (o memoria) y devuelve una textura OpenGL lista para samplear desde un shader. Vendorea [stb_image](https://github.com/nothings/stb) en `cpp/vendor/stb/`. + +## API + +```cpp +namespace fn { + +struct GlTexture { + GLuint id = 0; + int w = 0; + int h = 0; + int channels = 0; + bool ok() const { return id != 0; } +}; + +GlTexture gl_texture_load(const char* path, bool flip_y = true, bool srgb = false); +GlTexture gl_texture_load_from_memory(const unsigned char* data, int size, + bool flip_y = true, bool srgb = false); + +void gl_texture_destroy(GlTexture& tex); +const char* gl_texture_last_error(); +void gl_texture_bind_uniform(GLuint program, const char* name, + const GlTexture& tex, int unit); + +} // namespace fn +``` + +## Uso + +```cpp +#include "gfx/gl_texture_load.h" + +auto tex = fn::gl_texture_load("assets/noise.png"); +if (!tex.ok()) { + std::fprintf(stderr, "error: %s\n", fn::gl_texture_last_error()); + return 1; +} + +glUseProgram(prog); +fn::gl_texture_bind_uniform(prog, "u_noise", tex, /*unit=*/0); +glDrawArrays(GL_TRIANGLES, 0, 6); + +// al destruir: +fn::gl_texture_destroy(tex); +``` + +## Comportamiento + +- Forzamos 4 canales (RGBA) al decodificar via stb (`req_comp=4`), asi el upload a `glTexImage2D` siempre es regular. +- Filtros: `GL_LINEAR_MIPMAP_LINEAR` (min) + `GL_LINEAR` (mag). Mipmaps generados con `glGenerateMipmap`. +- Wrap: `GL_REPEAT` en S y T. +- HDR (`.hdr`): `stbi_loadf` + `GL_RGBA16F` (driver con GL 3.0+). En este caso `srgb` se ignora. +- sRGB (LDR + `srgb=true`): internal format `GL_SRGB8_ALPHA8`. El driver convierte a linear automaticamente al samplear. +- `flip_y=true` por defecto — la mayoria de PNGs vienen con (0,0) arriba; OpenGL espera (0,0) abajo. + +## Errores + +- En fallo, `id == 0` y `ok() == false`. El detalle (string de stb o "glGenTextures returned 0") se guarda en un `thread_local` accesible via `gl_texture_last_error()` hasta el siguiente `gl_texture_load*` en el mismo thread. + +## Limites + +- Tamano practico: <= 8192 px por lado (depende del driver — `GL_MAX_TEXTURE_SIZE`). +- No reentrante con la misma stb-flag global: stb usa una variable global para `flip_vertically`. Si dos threads cargan en paralelo, el resultado puede mezclar los flags. Para paralelo serio, serializar las llamadas o usar `stbi__ldr_to_hdr` directo con flag local. + +## Composicion + +Depende de `gl_loader_cpp_gfx` para los simbolos `glActiveTexture`, `glGenerateMipmap`, `glUseProgram`, `glGetUniformLocation`, `glUniform1i` (en Linux son simbolos directos via `GL_GLEXT_PROTOTYPES`; en Windows se resuelven con `wglGetProcAddress`). + +Compone naturalmente con: +- `gl_shader_cpp_gfx` — para usar la textura en un fragment shader (uniform sampler2D). +- `shader_canvas_cpp_gfx` — un canvas full-screen que samplea la textura con uniforms de tint/zoom. + +## Atribucion + +stb_image v2.30 — public domain (MIT-0). Ver `cpp/vendor/stb/README.md`.