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 字节)。

这通常分为两个步骤:

  1. 将图像从磁盘加载到内存(RAM)中。在本示例中,我们会将图像解压缩为 RGBA 格式的图像。你可以使用 stb_image.h 等辅助库来完成这项工作;

  2. 将内存中解压缩后的原始 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();

1-hupc.webp

关于纹理坐标(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.0f1.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)关联(使用 SliderFloat2DragFloat2),这样你就能实时调整这些坐标值,从而更直观地理解它们的含义。

如果你想显示同一张图像但进行缩放,只需保持 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));

4-bswq.webp

修改渲染状态

你可以使用绘制回调(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);