How to Use ImTextureID with Custom Texture Types in OpenGL: A Complete Guide

Dear ImGui's ImTextureID is an opaque void* value that the OpenGL backends interpret as a GLuint texture name, allowing you to pass raw texture handles, custom wrapper classes, or complex resource managers as long as the value round-trips through void* → intptr_t → GLuint without data loss.

Integrating custom texture abstractions with Dear ImGui requires understanding how ImTextureID bridges your application code with the rendering backend. In the ocornut/imgui repository, this opaque type provides flexibility for OpenGL applications to use raw texture names, C++ object wrappers, or smart pointers while maintaining compatibility with ImGui's immediate-mode drawing API. This guide explains the exact mechanism used by the OpenGL backends and provides working patterns for custom texture integration.

Understanding ImTextureID in Dear ImGui

ImTextureID is defined in imgui.h as a void* typedef, making it an opaque pointer-sized value that ImGui treats as a black box. The library itself never dereferences this pointer; instead, it passes the value directly to the rendering backend during draw-list execution. This design allows the same ImGui code to work across DirectX, Vulkan, Metal, and OpenGL, where the underlying texture representation differs significantly.

For OpenGL specifically, the backend implementation in backends/imgui_impl_opengl3.cpp (and the legacy backends/imgui_impl_opengl2.cpp) expects this opaque value to represent an OpenGL texture name (GLuint). The backend extracts the actual texture handle using a specific casting pattern that preserves integer values through pointer storage.

How the OpenGL Backend Interprets ImTextureID

The OpenGL backend converts the opaque ImTextureID back to a usable GLuint through a two-step cast designed to prevent compiler warnings while preserving data:

// Located in backends/imgui_impl_opengl3.cpp
GLuint texture_id = (GLuint)(intptr_t)user_texture_id;

This conversion sequence is critical: intptr_t ensures the integer texture name survives the round-trip through a pointer-sized storage. Therefore, any custom texture type you create must produce a value that can be cast to void* (as ImTextureID) and then back to GLuint through intptr_t without truncation or alteration.

Integration Patterns for Custom Texture Types

The flexibility of the opaque handle allows several architectural patterns. Choose the approach that best fits your resource management strategy while adhering to the casting contract.

Raw GLuint Texture Names

The simplest approach stores the OpenGL texture name directly. This works immediately with zero overhead and is the pattern used in the official examples like examples/example_glfw_opengl3/main.cpp.

GLuint my_gl_tex;
glGenTextures(1, &my_gl_tex);
glBindTexture(GL_TEXTURE_2D, my_gl_tex);
// Upload pixel data with glTexImage2D...

// Cast to ImTextureID for ImGui
ImTextureID tex_id = (ImTextureID)(intptr_t)my_gl_tex;

ImGui::Begin("Texture Demo");
ImGui::Image(tex_id, ImVec2(256.0f, 256.0f));
ImGui::End();

Custom C++ Structs and Classes

For applications using wrapper objects (e.g., a Texture2D class), you have two valid approaches:

Pass the GLuint handle directly (recommended): Extract the raw texture name via a getter method and cast it to ImTextureID at the call site. This keeps ImGui calls clean and avoids pointer indirection.

Pass a pointer to the wrapper: Store the address of your texture object as the ImTextureID, then dereference it when needed. This is useful when you need additional metadata (width, height, format) alongside the texture ID.

struct MyTexture
{
    GLuint gl_id;
    int width, height;
    
    MyTexture(const char* filename) {
        // Load image, glGenTextures, glTexImage2D...
        glGenTextures(1, &gl_id);
        // ... upload logic
    }
    
    GLuint GetGLHandle() const { return gl_id; }
};

MyTexture myTex("awesome.png");

// Approach A: Pass raw handle
ImGui::Image((ImTextureID)(intptr_t)myTex.GetGLHandle(),
             ImVec2((float)myTex.width, (float)myTex.height));

// Approach B: Pass pointer to wrapper
MyTexture* texPtr = &myTex;
ImGui::ImageButton(static_cast<ImTextureID>(texPtr), ImVec2(128, 128));

Smart Pointer Integration

When using smart pointers like std::shared_ptr or std::unique_ptr, store the raw pointer value obtained via .get(). Do not pass the smart pointer object itself, as that would slicing-cast the control block.

std::shared_ptr<MyTexture> texPtr = std::make_shared<MyTexture>("logo.png");
ImTextureID imgui_id = static_cast<ImTextureID>(texPtr.get());

ImGui::Image(imgui_id, ImVec2(200.0f, 200.0f));

// Later retrieval (if you stored the pointer):
if (ImGui::ImageButton(imgui_id, ImVec2(100, 100)))
{
    MyTexture* tex = static_cast<MyTexture*>(imgui_id);
    GLuint actual_id = tex->GetGLHandle();
    // Use actual_id for manual binding if needed
}

Practical Implementation Examples

The following patterns demonstrate complete, compile-ready workflows for different abstraction levels.

Example 1: Basic Raw GLuint Workflow

// Create and configure OpenGL texture
GLuint my_gl_tex;
glGenTextures(1, &my_gl_tex);
glBindTexture(GL_TEXTURE_2D, my_gl_tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// ... upload with glTexImage2D ...

// Convert to ImTextureID using intptr_t cast
ImTextureID tex_id = (ImTextureID)(intptr_t)my_gl_tex;

// Use in ImGui
ImGui::Begin("Raw Texture");
ImGui::Image(tex_id, ImVec2(256.0f, 256.0f));
ImGui::End();

Example 2: Wrapper Class with Metadata

class Texture2D
{
    GLuint m_id;
public:
    int Width, Height;
    
    Texture2D(const char* path) : Width(0), Height(0) {
        // Load file, get dimensions
        glGenTextures(1, &m_id);
        glBindTexture(GL_TEXTURE_2D, m_id);
        // ... upload ...
    }
    
    GLuint GetID() const { return m_id; }
    ~Texture2D() { glDeleteTextures(1, &m_id); }
};

// Usage
Texture2D playerSprite("player.png");
ImTextureID player_img = (ImTextureID)(intptr_t)playerSprite.GetID();

ImGui::Image(player_img, ImVec2((float)playerSprite.Width, 
                                (float)playerSprite.Height));

Example 3: Pointer-to-Wrapper Pattern

struct GameTexture {
    GLuint gl_id;
    std::string debugName;
    float lastAccessTime;
};

GameTexture* hudTexture = new GameTexture(); // In practice, use your pool allocator
hudTexture->gl_id = loadTexture("hud.png"); // Your loading function

ImTextureID hud_id = static_cast<ImTextureID>(hudTexture);

ImGui::Begin("HUD");
if (ImGui::ImageButton("hud_btn", hud_id, ImVec2(64, 64)))
{
    // Retrieve wrapper to access metadata
    GameTexture* tex = static_cast<GameTexture*>(hud_id);
    std::cout << "Clicked texture: " << tex->debugName << std::endl;
}
ImGui::End();

Example 4: Smart Pointer Safety

auto sharedTex = std::make_shared<MyTexture>("ui_icon.png");
ImTextureID tex_id = static_cast<ImTextureID>(sharedTex.get());

// Ensure sharedTex remains alive while ImGui renders
ImGui::Image(tex_id, ImVec2(32.0f, 32.0f));

// If you need to ensure the resource stays alive through the frame,
// keep the shared_ptr in a temporary variable or frame-bound storage

Key Source Files and References

Understanding the contract between ImGui and the OpenGL renderer requires examining these specific files in the ocornut/imgui repository:

File Relevance
backends/imgui_impl_opengl3.cpp Contains the critical conversion (GLuint)(intptr_t)user_texture_id used to extract texture handles from ImTextureID.
backends/imgui_impl_opengl2.cpp Implements identical extraction logic for legacy OpenGL 2.x contexts.
imgui.h Defines ImTextureID as void* and declares ImGui::Image(), ImGui::ImageButton(), and other texture-consuming functions.
examples/example_glfw_opengl3/main.cpp Demonstrates practical texture loading with GLFW/GLAD and proper ImTextureID casting.
examples/example_sdl2_opengl3/main.cpp Alternative example showing SDL2 integration with OpenGL texture handling.

Summary

  • ImTextureID is an opaque void* handle that ImGui passes through to the rendering backend without interpretation.
  • The OpenGL backends in imgui_impl_opengl3.cpp expect the value to cast safely to GLuint via (GLuint)(intptr_t).
  • You can pass raw GLuint texture names directly by casting through intptr_t.
  • For custom classes, either expose a GetGLHandle() method and pass the result, or pass the object pointer if you need runtime metadata retrieval.
  • When using smart pointers, store the raw pointer from .get() as the ImTextureID, ensuring the managed object outlives the ImGui render frame.
  • Never store complex objects directly in ImTextureID; the value must fit in a pointer-sized integer and round-trip through the intptr_t conversion without data loss.

Frequently Asked Questions

What is the ImTextureID type in ImGui?

ImTextureID is defined in imgui.h as a void* typedef, serving as an opaque handle that ImGui treats as a black box. The library never dereferences it; instead, it passes the value to the active rendering backend (OpenGL, DirectX, Vulkan, etc.), which interprets the value according to its own graphics API requirements. In OpenGL backends, this value represents a GLuint texture name.

Can I pass a C++ class directly to ImTextureID?

You cannot pass a C++ class instance by value to ImTextureID, but you can pass a pointer to your class instance. Store the address of your texture wrapper object as the ImTextureID using static_cast<ImTextureID>(&myObject), then cast it back to your type when needed. Alternatively, extract the raw GLuint from your class and pass that directly through the required intptr_t cast.

How does the OpenGL backend convert ImTextureID to GLuint?

The OpenGL implementation in backends/imgui_impl_opengl3.cpp extracts the texture ID using the expression (GLuint)(intptr_t)user_texture_id. This two-stage cast first converts the void* to intptr_t (an integer type guaranteed to hold pointer values), then to GLuint. This ensures that integer texture names survive round-trip storage in a pointer-sized value without compiler warnings or data truncation.

Is ImTextureID compatible with other graphics APIs?

Yes. The opaque nature of ImTextureID makes it API-agnostic. While OpenGL uses GLuint texture names, the DirectX backends use ID3D11ShaderResourceView*, Vulkan implementations might use VkImageView handles, and Metal uses MTLTexture pointers. The same ImGui code calling ImGui::Image() works across all backends because each implementation interprets the void* according to its specific graphics API contract.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →