https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
TL;DR;
将图像文件加载到 GPU 纹理中,不属于 Dear ImGui 的功能范畴,而更多与你所使用的图形 API(Graphics API) 相关。由于这是 Dear ImGui 用户经常遇到的问题,我们在此提供一份指南。
本指南将介绍如何从磁盘加载图像文件,并在 Dear ImGui 窗口中显示该图像。
我们将加载以下图像:(右键保存为 MyImage01.jpg,文件大小 20,123 字节)。
这通常分为两个步骤:
将图像从磁盘加载到内存(RAM)中。在本示例中,我们会将图像解压缩为 RGBA 格式的图像。你可以使用 stb_image.h 等辅助库来完成这项工作;
将内存中解压缩后的原始 RGBA 图像加载到 GPU 纹理中。这一步需要使用你所选用的图形 API(如 OpenGL、DirectX11 等)的专用函数来实现。
一旦图像已存入 GPU 纹理内存,你就可以使用 ImGui::Image ()
等函数,让 Dear ImGui 创建一个绘制命令,然后由 Dear ImGui 的渲染后端将其转换为实际的绘制调用(draw call)。
注意:大型游戏和应用程序可能会使用在 GPU 上经过压缩的纹理格式,或采用更复杂的技术,这些内容超出了本文的讨论范围。通常情况下,如果你正在阅读本文,无需担心或关注这类内容。
将游戏场景渲染到纹理中
本指南涵盖了从磁盘加载图像文件并将其转换为 GPU 纹理的内容。纹理的另一个常见用途是,将应用程序 / 游戏场景渲染到纹理中,然后在 Dear ImGui 窗口内显示该纹理。
本指南不涉及这一用途,但你应先阅读本指南,以更好地理解纹理的工作原理。之后,你可以查阅有关 “渲染到纹理”(rendering to a texture)的相关资料。
文件
请注意,许多 C/C++ 新手会因提供的文件名错误而遇到文件相关问题。
需注意两点:
在 C/C++ 及大多数编程语言中,若要在字符串常量中使用反斜杠
\
,必须写成两个反斜杠\\
(即转义字符)。而恰巧 Windows 系统使用反斜杠作为路径分隔符,因此需特别留意这一点。
filename = "C:\MyFiles\MyImage01.jpg"; // This is INCORRECT!!
filename = "C:\\MyFiles\\MyImage01.jpg"; // This is CORRECT
在某些情况下,你在 Windows 系统下也可以使用正斜杠 /
作为路径分隔符。
请确保你的 IDE 或调试器的设置,能从正确的工作目录启动可执行文件。在 Visual Studio 中,你可以在
Properties
>General
>Debugging
>Working Directory
中修改工作目录。人们通常会默认程序会从项目的根目录启动,但默认情况下,程序往往会从目标文件 (object files) 或可执行文件所在的文件夹启动。
// Relative filename depends on your Working Directory when running your program!
filename = "MyImage01.jpg";
filename = "../MyImage01.jpg"; // Load from parent folder
ImTextureId
简短介绍:
你可以使用
ImGui::Image()
,ImGui::ImageButton()
等函数,或者更底层的ImDrawList::AddImage()
函数来生成使用自定义纹理的绘制调用。通过ImGui::GetBackgroundDrawList()
,你可以提交AddImage()
调用,这些调用不属于特定的 ImGui 窗口,而是显示在背景内容和 ImGui 窗口之间;实际纹理的标识方式由用户或引擎决定。这些标识符会被存储,并以
ImTextureID
类型的值传递;从磁盘加载图像文件并将其转换为纹理,并不在 Dear ImGui 的功能范围内。你可以查阅所使用图形 API 的文档或教程,了解如何上传纹理。在本文档的后续部分,你会找到相关示例。
详细介绍:
Dear ImGui 的职责是创建 “meshes”,这些网格以与渲染器无关的格式定义,由绘制命令和顶点组成。在每一帧结束时,这些 meshes(ImDrawList)将通过你的渲染函数进行显示。它们由带纹理的多边形构成,而用于渲染它们的代码通常相当简短(几十行而已)。在 examples 文件夹中,我们提供了适用于主流图形 API(如 OpenGL、DirectX 等)的函数。
每个渲染函数都会确定一种用于表示 “textures” 的数据类型。“texture” 的概念完全与你所使用的底层引擎或图形 API 绑定。我们通过
ImTextureID
类型来携带标识 “texture” 的信息。ImTextureID
默认是ImU64
类型,也就是 8 字节的数据:刚好足以存储一个指针或一个你选择的整数(注意:在 1.91.3 版本之前,ImTextureID
默认是void*
类型,在 32 位构建中无法容纳例如 64 位描述符之类的数据)。Dear ImGui 并不知晓或理解你在ImTextureID
中存储了什么,它只是传递ImTextureID
的值,直到这些值到达你的渲染函数。在 examples/bindings,对于每一个图形 API 的绑定,我们都确定了一种从终端用户视角来看,很适合用于指定图像的类型。以下就是示例渲染函数所使用的(类型):
OpenGL: ImTextureID = GLuint (see ImGui_ImplOpenGL3_RenderDrawData() in imgui_impl_opengl3.cpp)
DirectX9: ImTextureID = LPDIRECT3DTEXTURE9 (see ImGui_ImplDX9_RenderDrawData() in imgui_impl_dx9.cpp)
DirectX11: ImTextureID = ID3D11ShaderResourceView* (see ImGui_ImplDX11_RenderDrawData() in imgui_impl_dx11.cpp)
DirectX12: ImTextureID = D3D12_GPU_DESCRIPTOR_HANDLE (see ImGui_ImplDX12_RenderDrawData() in imgui_impl_dx12.cpp)
SDL_Renderer: ImTextureID = SDL_Texture* (see ImGui_ImplSDLRenderer2_RenderDrawData() in imgui_impl_sdlrenderer2.cpp)
Vulkan: ImTextureID = VkDescriptorSet (see ImGui_ImplVulkan_RenderDrawData() in imgui_impl_vulkan.cpp)
WebGPU: ImTextureID = WGPUTextureView (see ImGui_ImplWGPU_RenderDrawData() in imgui_impl_wgpu.cpp)
例如,在 OpenGL 示例绑定代码中,我们会将原始的 OpenGL 纹理标识符(即 GLuint
类型)存储在 ImTextureID
中。而在 DirectX11 示例绑定代码中,我们会将指向 ID3D11ShaderResourceView
的指针存储在 ImTextureID
中 —— ID3D11ShaderResourceView
是一种更高级的结构,它既关联了纹理本身,也包含了纹理的格式信息以及如何读区该纹理的相关配置。
如果你基于 OpenGL 等接口构建了自定义引擎,那么你或许不会直接传递
GLuint
,而是选择使用一种高级数据类型来携带纹理信息以及显示方式(如着色器等相关配置)。决定将什么用作ImTextureID
,最好结合你代码库的设计来考虑。如果你的引擎中已经有用于”纹理”和”材质”的高级数据类型,那么或许就应该使用这些类型。如果你刚开始使用 OpenGL、DirectX 或 Vulkan,还没有在它们之上构建太多渲染引擎相关内容,那么保留示例绑定中建议的默认ImTextureID
表示可能是最佳选择。(高级用户也可以决定在ImTextureID
中保留低级类型,并使用ImDrawList
回调函数将信息传递给渲染器)用户代码可以这样:
// Cast our texture type to ImTextureID
MyTexture* texture = g_CoffeeTableTexture;
ImGui::Image((ImTextureID)(intptr_t)texture, ImVec2(texture->Width, texture->Height));
在调用 ImGui::Render()
之后执行的渲染函数,将会接收到用户代码所传递的同一个值:
// Cast ImTextureID stored in the draw command as our texture type
MyTexture* texture = (MyTexture*)(intptr_t)pcmd->TextureId;
MyEngineBindTexture2D(texture);
一旦理解了这种设计,你就能明白:从磁盘加载图像文件并将其转换为可显示的纹理,并不在 Dear ImGui 的功能范围内。这是刻意设计的结果,而且实际上是件好事——因为这意味着你的代码可以完全控制自己的数据类型,以及如何显示这些数据。实际上,”texture” 的定义在很大程度上是开放的(没有固定标准),而 Dear ImGui 并不想限制这个概念的范围。如果你希望在屏幕上显示图像文件(例如 PNG 文件),请参考之前的案例。
最后,你可以调用 ImGui::ShowMetricsWindow()
函数,以探索、可视化并理解 ImDrawList
(绘制列表)是如何生成的。
OpenGL 的例子
使用 stb_image.h 从磁盘上加载图片
#define _CRT_SECURE_NO_WARNINGS
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
// 简单的帮助函数,加载图片到 OpenGL texture
bool LoadTextureFromMemory(const void* data,
size_t data_size,
GLuint* out_texture,
int* out_width, int* out_height)
{
// Load from file
int image_width = 0;
int image_height = 0;
unsigned char* image_data = stbi_load_from_memory(
(const unsigned char*)data,
(int)data_size,
&image_width, &image_height,
NULL, 4);
if(image_data == NULL)
return false;
// Create a OpenGL texture identifier
GLuint image_texture;
glGenTextures(1, &image_texture);
glBindTexture(GL_TEXTURE_2D, image_texture);
// Setup filtering parameters for display
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// Upload pixels into texture
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image_width, image_height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, image_data);
stbi_image_free(image_data);
*out_texture = image_texture;
*out_width = image_width;
*out_height = image_height;
}
// Open and read a file, then forward to LoadTextureFromMemory()
bool LoadTextureFromFile(const char* file_name, GLuint* out_texture,
int* out_width, int* out_height)
{
FILE* f = fopen(file_name, "rb");
if(f == NULL)
return false;
fseek(f, 0, SEEK_END);
size_t file_size = (size_t)ftell(f);
if(file_size == -1)
return false;
fseek(f, 0, SEEK_SET);
void* file_data = IM_ALLOC(file_size);
fread(file_data, 1, file_size, f);
fclose(f);
bool ret = LoadTextureFromMemory(file_data,
file_size,
out_texture,
out_width,
out_height);
IM_FREE(file_data);
return ret;
}
在初始化 OpenGL 加载器之后(例如调用 glewInit()
之后),再加载纹理:
int my_image_width = 0;
int my_image_height = 0;
GLuint my_image_texture = 0;
bool ret = LoadTextureFromFile("../../MyImage01.jpg", &my_image_texture,
&my_image_width, &my_image_height);
IM_ASSERT(ret);
在主循环中(main loop)中显示这个纹理:
ImGui::Begin("OpenGL Texture Text");
ImGui::Text("pointer = %x", my_image_texture);
ImGui::Text("size = %d x %d", my_image_width, my_image_height);
ImGui::Image((ImTextureID)(intptr_t)my_image_texture,
ImVec2(my_image_width, my_image_height));
ImGui::End();
关于纹理坐标(Texture Coordinates)
在本节内容中,”纹理坐标”(Texture Coordinates)与“UV 坐标”(UV Coordinates)这两个术语可以互换使用。
ImGui::Image()
和 ImDrawList::AddImage()
这两个函数允许你传递”UV 坐标”,该坐标对应你想要显示的纹理的左上角和右下角区域。若使用默认值(这两个坐标分别为 (0.0f, 0.0f)
和 (1.0f, 1.0f)
,则可以显示整个底层纹理。UV 坐标通常是标准化坐标,这意味着对于每个坐标轴(U 轴和 V 轴),我们不是通过统计纹理像素(texel)的数量来定位,而是用 0.0f
到 1.0f
之间的数值来表示纹理中的位置。因此, (0.0f, 0.0f)
通常指向纹理的左上角区域, (1.0f, 1.0f)
则指向纹理的右下角区域。不过,部分图形系统可能会使用 Y 轴(即 V 轴)方向颠倒的坐标(例如 0.0f, 0.0f
对应右下角、 (1.0f, 1.0f)
对应左上角)。
// Default coordinates
ImGui::Image((ImTextureID)(intptr_t)my_texture,
ImVec2(256, 256),
ImVec2(0.0f, 0.0f),
ImVec2(1.0f, 1.0f));
ImGui::Text("Flip Y coordinates:");
// Flip Y coordinates
ImGui::Image((ImTextureID)(intptr_t)my_texture,
ImVec2(256, 256),
ImVec2(0.0f, 1.0f),
ImVec2(1.0f, 0.0f));
如果你想显示纹理的一部分(例如,在一张 256x256
的纹理中,显示从像素 (10, 10)
到像素 (110, 210)
的 100x200
矩形区域,就需要计算这些像素对应的标准化坐标:
// Normalized coordinates of pixel (10, 10) in a 256x256 texture
ImVec2 uv0 = ImVec2(10.0f/256.0f, 10.0f/256.0f);
// Normalized coordinates of pixel (110, 210) in a 256x256 texture
ImVec2 uv1 = ImVec2((10.0f+100.0f)/256.0f, (10.0f+200.0f)/256.0f);
ImGui::Text("uv0 = (%f, %f)", uv0.x, uv0.y);
ImGui::Text("uv1 = (%f, %f)", uv1.x, uv1.y);
// Display the 100x200 section starting at (10, 10)
ImGui::Image((ImTextureID)(intptr_t)my_image_texture,
ImVec2(100.0f, 200.0f),
uv0,
uv1);
另外一种写法:
ImVec2 display_min = ImVec2(10.0f, 10.0f);
ImVec2 display_size = ImVec2(100.0f, 200.0f);
ImVec2 texture_size = ImVec2(256.0f, 256.0f);
ImVec2 uv0 = ImVec2(display_min.x / texture_size.x,
display_min.y / texture_size.y);
ImVec2 uv1 = ImVec2((display_min.x + display_size.x) / texture_size.x,
(display_min.y + display_size.y) / texture_size.y);
ImGui::Text("uv0 = (%f, %f)", uv0.x, uv0.y);
ImGui::Text("uv1 = (%f, %f)", uv1.x, uv1.y);
ImGui::Image((ImTextureID)(intptr_t)my_image_texture,
ImVec2(display_size.x, display_size.y),
uv0, uv1);
小技巧:将你的 UV 坐标与控件(widget)关联(使用 SliderFloat2
或 DragFloat2
),这样你就能实时调整这些坐标值,从而更直观地理解它们的含义。
如果你想显示同一张图像但进行缩放,只需保持 UV 坐标不变,修改尺寸(Size)参数即可:
// Normal size
ImGui::Image((ImTextureID)(intptr_t)my_image_texture,
ImVec2(my_image_width, my_image_height),
ImVec2(0.0f, 0.0f),
ImVec2(1.0f, 1.0f));
// Half size, same contents
ImGui::Image((ImTextureID)(intptr_t)my_image_texture,
ImVec2(my_image_width*0.5f, my_image_height*0.5f),
ImVec2(0.0f, 0.0f),
ImVec2(1.0f, 1.0f));
修改渲染状态
你可以使用绘制回调(draw callbacks)来操作渲染状态,例如修改采样方式(sampling)。自 1.91.3 版本起,backends 会在渲染循环中暴露部分自身状态,这使得利用 callbacks 变得更加容易。请参考各个 backend 的 .h 头文件,查看是否暴露了 ImGui_ImplXXXX_RenderState
结构体。
可通过类似如下的代码访问这些状态:
// For DX11 backend: Callback to modify current sampler
void ImDrawCallback_ImplDX11_SetSampler(const ImDrawList* parent_list,
const ImDrawCmd* cmd)
{
ImGui_ImplDX11_RenderState* state =
(ImGui_ImplDX11_RenderState*)ImGui::GetPlatformIO().Renderer_RenderState;
ID3D11SamplerState* sampler = cmd->UserCallbackData ?
(ID3D11SamplerState*)cmd->UserCallbackData :
state->SamplerDefault;
state->DeviceContext->PSSetSamplers(0, 1, &sampler);
}
// For DX11 backend: Create a Point sampler
{
D3D11_SAMPLER_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;
desc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
desc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
desc.MipLODBias = 0.f;
desc.ComparsionFunc = D3D11_COMPARSION_ALWAYS;
desc.MinLOD = 0.f;
desc.MaxLOD = 0.f;
g_pd3dDevice->CreateSamplerState(&desc, &g_SamplerPoint);
}
// Set custom sampler
ImGui::GetWindowDrawList()->AddCallback(ImDrawCallback_ImplDX11_SetSampler,
g_SamplerPoint);
ImGui::Image((ImTextureID)(intptr_t)my_texture, ImVec2((float)my_image_width * 4,
(float)my_image_height * 4));
// Restore sampler
ImGui::GetWindowDrawList()->AddCallback(ImDrawCallback_ImplDX11_SetSampler, NULL);