SoftGLRender源码:配置面板(ConfigPanel)

Config类

特性

文件:Config.h

Config类管理着SoftGLRender的配置资源,主要包括资源路径、渲染设置、场景参数等.

Config对象由ViewManager管理,但由ConfigPanel使用. 因此,介绍ConfigPanel时,需要先了解Config.

常量定义

资源路径

// 资源路径
const std::string ASSERTS_DIR    = "./assets"; // 模型、贴图等路径
const std::string SHADER_GLSL_DR = "./shader/GLSL"; // 着色器路径

抗锯齿类型

抗锯齿技术可减少图形中锯齿状边缘. 2种常见抗锯齿技术:

  • MSAA 多重采样抗锯齿,对多边形的边缘进行多重采样(如4x、8x),通过混合相邻像素的颜色平滑锯齿. 仅对几何边缘处理,不处理纹理内部的锯齿.
    质量较好,但速度慢.

  • FXAA 快速近似抗锯齿,后处理的全屏模糊技术,通过快速模糊整个画面中的高对比度区域(包括边缘和纹理)来消除锯齿.
    质量一般,但速度快.

// 抗锯齿类型
enum AAType {
	AAType_NONE, // 无抗锯齿
	AAType_MSAA, // 多重采样抗锯齿
	AAType_FXAA, // 快速近似抗锯齿
};

Config类定义

class Config {
public:
    // resource paths
    // 管理当前加载的3D模型和skybox资源
	std::string modelName;
	std::string modelPath;
	std::string skyboxName;
	std::string skyboxPath;

    // 模型的三角形总数
	size_t triangleCount_ = 0;

    // 显示控制
	bool wireFrame  = false; // 是否启用线框模式
	bool worldAxis  = true;  // 是否显示世界坐标系
	bool showSkybox = false; // 是否显示天空盒
	bool showFloor  = false; // 是否显示地面

	bool shadowMap  = true;  // 是否启用阴影贴图
	bool pbrIbl     = false; // 是否启用基于图像的PBR光照
	bool mipmaps    = false; // 是否生成纹理Mipmap

	bool cullFace   = true;  // 是否启用背面剔除
	bool depthTest  = true;  // 是否启用深度测试
	bool reverseZ   = false; // 反转Z缓冲

	glm::vec4 clearColor   = { 0.f, 0.f, 0.f, 0.f }; // 清屏颜色(RGBA)
	glm::vec3 ambientColor = { 0.5f, 0.5f, 0.5f }; // 环境光颜色(RGB)

	bool showLight = true;
	glm::vec3 pointLightPosition = { 0.f, 0.f, 0.f }; // 点光源位置
	glm::vec3 pointLightColor = { 0.5f, 0.5f, 0.5f }; // 点光源颜色

    // render mode
	int aaType     = AAType_NONE; // 抗锯齿类型(默认关闭)
	int renderType = Renderer_SOFT; // 渲染器类型(默认为软光栅化)
};

reverseZ 反转深度缓冲范围,可减少远距离物体的Z-fighting问题.

pbrIbl 基于物理的渲染(PBR),基于图像的光照(IBR).

ConfigPanel类

特性

文件:ConfigPanel.h, ConfigPanel.cpp

ConfigPanel是一个UI强相关的类,主要负责利用配置(Config)信息构建配置面板界面,同时为用户提供交换接口.

下图是SoftGLRender初始界面:

img

ConfigPanel数据成员

private:
	Config &config_; // not the owner

    // frame size
	int frameWidth_ = 0;
	int frameHeight_ = 0;

	float lightPositionAngle_ = glm::radians(235.f); // 角度 -> 弧度

	std::unordered_map<std::string, std::string> modelPaths_;
	std::unordered_map<std::string, std::string> skyboxPaths_;

	std::vector<const char*> modelNames_;
	std::vector<const char*> skyboxNames_;

	// callbacks
	std::function<bool(const std::string& path)> reloadModelFunc_;
	std::function<bool(const std::string& path)> reloadSkyboxFunc_;
	std::function<void(glm::vec3& position, glm::vec3& color)> updateLightFunc_;
	std::function<void(void)> resetCameraFunc_;
	std::function<void(void)> resetMipmapFunc_;
	std::function<void(void)> resetReverseZFunc_;
	std::function<void(void)> frameDumpFunc_;

重用数据成员:

  • config_,ConfigPanel类利用Config对象来配置界面,但本身并不持有Config对象,Config对象由ViewManager管理,同时,ConfigPanel对象也是被ViewManager管理,因此生存周期一致,只需要一个引用即可.
  • lightPositionAngle_ 控制点光源位置的时候,是通过一个滑块控制角度lightPositionAngle_来实现的.
  • modelPaths_, skyboxPaths_ 模型、天空盒资源路径. 路径和名称以json格式存放在json文件中.
  • modelNames_, skyboxNames_ 模型、天空盒资源名称.

ConfigPanel函数成员

核心函数:

  • onDraw() 主循环调用,绘制面板
  • update() 主循环调用,更新光源位置
  • wantCaptureKeyboard() 检测ImGUI是否希望捕获键盘输入
  • wantCaptureMouse() 检测ImGUI是否希望捕获鼠标输入
  • loadConfig() 根据Config模块信息,读取资源路径
  • reloadModel() 重载模型
  • reloadSkybox() 重载skybox
  • drawSettings() 负责实现绘制Settings面板内容

注意:ConfigPanel只负责读取资源路径,并转存给Config对象,并不直接读取模型文件. 也就是说,ConfigPanel不负责3D模型绘制.

构造与析构

ConfigPanel的构造函数很简单,从ViewManager接受Config对象,并绑定本地引用.

explicit ConfigPanel(Config &config) : config_(config) {}

ConfigPanel类的真正初始化,不是在ctor中,而是在init()中.
这部分主要工作:
1)初始化ImGui环境(见下文ImGui使用流程);
2)从Config模块加载资源路径;

注意:init不负责绘制界面

bool ConfigPanel::init(void* window, int width, int height) {
	frameWidth_ = width;
	frameHeight_ = height;

	// 1. 创建上下文
	// Setup Dear ImGui context
	IMGUI_CHECKVERSION(); // 版本兼容性检查宏
	ImGui::CreateContext();
	ImGuiIO& io = ImGui::GetIO(); 
	io.IniFilename = nullptr;

	// 2. 设置样式
	// Setup Dear ImGui style
	ImGui::StyleColorsDark(); // Optional: Dark / Classic / Light
	ImGuiStyle* style = &ImGui::GetStyle();
	style->Alpha = 0.8f;

	// 3. 初始化平台和渲染器绑定(示例: GLFW + OpenGL)
	ImGui_ImplGlfw_InitForOpenGL((GLFWwindow *)window, true);
	ImGui_ImplOpenGL3_Init("#version 330 core");

	// load config
	return loadConfig();
}

ConfigPanel::init()由ConfigPanel对象的拥有者ViewManager调用,这也是一种延迟初始化方式,可将类构造与初始化分离.

析构:主要回收在Init中申请的ImGui与OpenGL、glfw关联的资源

~ConfigPanel() { destroy(); };

void ConfigPanel::destroy() {
    // 关闭后端
    ImGui_ImplOpenGL3_Shutdown(); // 释放ImGui与OpenGL关联的资源, 与ImGui_ImplOpenGL3_Init配对
    ImGui_ImplGlfw_Shutdown(); // 是否ImGui与glfw关联的资源,与ImGui_ImplGlfw_InitForOpenGL配对

    // 销毁ImGui上下文
    ImGui::DestroyContext();
}

加载资源路径

loadConfiginit调用,主要负责从Config模块提供的信息加载资源路径.

bool ConfigPanel::loadConfig() {
	auto configPath = ASSERTS_DIR + "assets.json"; // "./assets/assets.json"
	auto configStr  = FileUtils::readText(configPath); // 一次性读取所有文本
	if (configStr.empty()) {
		LOGE("load models failed: error read config file");
		return false;
	}

    // 用json11库解析json文件
	std::string err;
	const auto json = json11::Json::parse(configStr, err);
    // 找到 model 下所有路径
	for (auto& kv : json["model"].object_items()) {
		modelPaths_[kv.first] = ASSERTS_DIR + kv.second["path"].string_value();
	}

    // 找到 skybox 下所有路径
	for (auto& kv : json["skybox"].object_items()) {
		skyboxPaths_[kv.first] = ASSERTS_DIR + kv.second["path"].string_value();
	}

	if (modelPaths_.empty()) {
		LOGE("load models failed: %s", err.c_str());
		return false;
	}

    // 找到所有model路径下资源对应名称
	for (const auto& kv : modelPaths_) {
		modelNames_.emplace_back(kv.first.c_str());
	}
    // 找到所有skybox路径下资源对应名称
	for (const auto& kv : skyboxPaths_) {
		skyboxNames_.emplace_back(kv.first.c_str());
	}

	// load default model & skybox
	return reloadModel(modelPaths_.begin()->first) && reloadSkybox(skyboxPaths_.begin()->first);
}

init中默认加载的model和skybox,是第一个从json文件读取到的资源.

"assets.json"文件,是以json格式存储了项目的模型(model)、天空盒(skybox)资源路径.

"assets.json"文件内容:

{
	"model": {
		"AfricanHead": {
			"path": "AfricanHead/african_head.obj"
		},
		"Brickwall": {
			"path": "Brickwall/brickwall.obj"
		},
		"Cube": {
			"path": "Cube/Cube.gltf"
		},
		"DamagedHelmet": {
			"path": "DamagedHelmet/DamagedHelmet.gltf"
		},
		"BoomBox": {
			"path": "BoomBox/BoomBox.gltf"
		},
		"GlassTable": {
			"path": "GlassTable/scene.gltf"
		},
		"Robot": {
			"path": "Robot/scene.gltf"
		}
	},
	"skybox": {
		"Lake": {
			"path": "Skybox/Lake/"
		},
		"Room": {
			"path": "Skybox/Room.jpeg"
		},
		"Hotel": {
			"path": "Skybox/Hotel.jpg"
		}
	}
}

加载model

加载model主要完成2个工作:

1)更新config配置中存放的mode name,model path信息;
2)回调用户定义的加载model函数;

// 加载指定名称的model
bool ConfigPanel::reloadModel(const std::string& name) {
	if (name != config_.modelName) {
        // 当前正在加载的model
		config_.modelName = name;
		config_.modelPath = modelPaths_[config_.modelName];

		if (reloadModelFunc_) {
			return reloadModelFunc_(config_.modelPath);
		}
	}
	return true;
}

加载skybox

与加载model方式类似,不过加载资源的来源不同.

bool ConfigPanel::reloadSkybox(const std::string& name) {
	if (name != config_.skyboxName) {
		config_.skyboxName = name;
		config_.skyboxPath = skyboxPaths_[config_.skyboxName];

		if (reloadSkyboxFunc_) {
			reloadModelFunc_(config_.skyboxPath);
		}
	}
	return true;
}

绘制面板

main函数通过调用viewer->drawPanel();,进而调用configPanel_->onDraw();绘制配置面板.

也就是说,实际绘制工作由onDraw完成. 这部分ImGui的工作,可参考下文ImGui使用流程中的主循环渲染.

// 绘制面板,主循环调用
inline void drawPanel() {
    if (showConfigPanel_) { // 如果启用了控制面板,则转发给onDraw()负责绘制工作
        configPanel_->onDraw();
    }
}

// 绘制面板
void ConfigPanel::onDraw() {
	// 1. 启动新帧
	// Start the Dear ImGui frame
	ImGui_ImplOpenGL3_NewFrame(); // 渲染器绑定的 new frame
	ImGui_ImplGlfw_NewFrame();    // 平台绑定的 new frame
	ImGui::NewFrame();            // ImGui 本身的 new frame

	// 2. 构建UI(核心逻辑)
	// 开始一个窗口
	ImGui::Begin(
		"Settings",
		nullptr, 
		ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize);
	drawSettings(); // 绘制设置菜单
	ImGui::SetWindowPos(ImVec2(frameWidth_ - ImGui::GetWindowWidth(), 0));
	ImGui::End(); // 结束窗口

	// 3. 渲染UI
	ImGui::Render();
	ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}

其中,隐藏了一个重要的自定义函数drawSettings,用于专门绘制设置菜单内容.

绘制设置菜单

drawSettings 负责绘制菜单. 与onDraw主要区别:

  • onDraw 绘制面板,同时为菜单绘制提供环境;
  • drawSettings 绘制面板中的菜单内容.

我们看下这个菜单包含哪些内容.

img

最上面窗口标题"Settings",是onDraw中设置的;从"renderer"文本开始,一直到最下面的"Aniti-aliasing"的3个选项,都由drawSettings负责.

  • renderer,包含3个单选选项{"Software","OpenGL","Vulkan"},用于选择渲染类型
  • camera,包含1个reset按钮,用于重置相机
  • debug,包含一个capture按钮,用于捕获画面(截图)
  • fps,显示当前帧数
  • triangles,显示当前模型的三角形总数
  • load model,1个下拉框,选择要加载的当前模型(名称)
  • load skybox,1个复选框,决定是否加载天空盒
  • clear color,4个文本输入框,双击可以输入RGBA值,决定清除色(背景色);右键可以选择颜色模型、颜色范围
  • wireframe,1个复选框,决定模型是否以网格形式显示
  • world axis,1个复选框,决定是否显示世界坐标(地板上的网格)
  • shadow floor,1个复选框,决定是否显示阴影地板
  • ambient color,3个文本输入框、1个颜色选择按钮,双击可输入RGBA值,决定环境光颜色;单击颜色选择按钮,可从颜色图中选择颜色;右键可以选择颜色模型、颜色范围(类似于clear color 部分)
  • point light,1个复选框,决定是否使用场景中的点光源
  • light color,点光源颜色. 设置方法类同ambient color
  • light position,1个滑动条,设置光源角度,进而决定光源位置
  • mipmaps,1个复选框,决定是否使用Mipmaps技术
  • cull face,1个复选框,决定是否开启背面剔除
  • depth test,1个复选框,决定是否开启深度测试
  • reverse z,1个复选框,决定是否z缓冲反向
  • Aniti-aliasing,3个单选框,决定使用哪种抗锯齿技术
// 绘制设置菜单
void ConfigPanel::drawSettings() {
	// renderer
	const char* rendererItems[] = {
		"Software",
		"OpenGL",
		"Vulkan",
	};

	ImGui::Separator(); // UI中插入一条水平分隔线
	ImGui::Text("renderer"); // UI中显示一段静态文本
	for (int i = 0; i < 3; i++) {
		if (ImGui::RadioButton(rendererItems[i], config_.renderType == i)) {
			config_.renderType = i;
		}
		ImGui::SameLine(); // 将下一个控件放在同一行
	}
	ImGui::Separator();

	// reset camera
	ImGui::Separator();
	ImGui::Text("camera");
	ImGui::SameLine();
	if (ImGui::SmallButton("reset")) {
		if (resetCameraFunc_) {
			resetCameraFunc_();
		}
	}

	// frame dump
	ImGui::Separator();
	ImGui::Text("debug (RenderDoc):");
	ImGui::SameLine();
	if (ImGui::SmallButton("capture")) {
		if (frameDumpFunc_) {
			frameDumpFunc_();
		}
	}

	// fps
	ImGui::Separator();
	ImGui::Text("fps: %.1f (%.2f ms/frame)", ImGui::GetIO().Framerate, 1000.f / ImGui::GetIO().Framerate);

	// model
	ImGui::Separator();
	ImGui::Text("load model");

	// find out current model
	int modelIdx = 0;
	for (; modelIdx < modelNames_.size(); modelIdx++) {
		if (config_.modelName == modelNames_[modelIdx]) {
			break;
		}
	}

	// ImGui::Combo: 创建下拉选择框(类似于HTML  <select>). 用户可以从预定义的选项列表中选择一个值
	if (ImGui::Combo("##load model", &modelIdx, modelNames_.data(), (int)modelNames_.size())) {
		reloadModel(modelNames_[modelIdx]);
	}

	// skybox
	ImGui::Separator();
	ImGui::Checkbox("load skybox", &config_.showSkybox);

	if (config_.showSkybox) {
		// pbr ibl
		// PBR: Physically Based Rendering,基于物理的渲染
		// IBL: Image-Based Lighting,基于图像的照明
		int skyboxIdx = 0;
		for (; skyboxIdx < skyboxNames_.size(); skyboxIdx++) {
			reloadSkybox(skyboxNames_[skyboxIdx]);
		}
	}

	// clear color
	ImGui::Separator();
	ImGui::Text("clear color");
	// ImGui::ColorEdit4: 提供一个 颜色选择器,允许用户通过UI交互修改一个RGBA颜色值(4个float,范围 [0.0, 1.0])
	// ImGuiColorEditFlags_NoLabel: 隐藏颜色选择器左侧的文本标签,仅保留颜色按钮和编辑区域
	ImGui::ColorEdit4("clear color", (float*)&config_.clearColor, ImGuiColorEditFlags_NoLabel);

	// wireframe
	ImGui::Separator();
	ImGui::Checkbox("wireframe", &config_.wireFrame);

	// world axis
	ImGui::Separator();
	ImGui::Checkbox("world axis", &config_.worldAxis);

	// shadow floor
	ImGui::Separator();
	ImGui::Checkbox("shadow floor", &config_.showFloor);
	config_.shadowMap = config_.showFloor;

	if (!config_.wireFrame) {
		// light
		ImGui::Separator();
		ImGui::Text("ambient color");
		// ImGui::ColorEdit3: 提供一个颜色选择器,用于编辑RGB颜色值(区别ImGui::ColorEdit4的RGBA值)
		ImGui::ColorEdit3("ambient color", (float*)&config_.ambientColor, ImGuiColorEditFlags_NoLabel);

		ImGui::Separator();
		ImGui::Checkbox("point light", &config_.showLight);
		if (config_.showLight) {
			ImGui::Text("light color");
			ImGui::ColorEdit3("light color", (float*)&config_.pointLightColor, ImGuiColorEditFlags_NoLabel);
		
			ImGui::Text("light position");
			// 通过滑块交互式地调整角度值(以弧度或度数为单位)
			// 直观地显示角度范围(如 -180° 到 180°),适合旋转、方向控制等场景
			ImGui::SliderAngle("##light position", &lightPositionAngle_, 0, 360.f);
		}

		// mipmaps
		ImGui::Separator();
		if (ImGui::Checkbox("mipmaps", &config_.mipmaps)) {
			if (resetMipmapFunc_) {
				resetMipmapFunc_();
			}
		}
	}

	// face cull
	ImGui::Separator();
	ImGui::Checkbox("cull face", &config_.cullFace);

	// depth test
	ImGui::Separator();
	ImGui::Checkbox("depth test", &config_.depthTest);

	// reverse Z
	ImGui::Separator();
	if (ImGui::Checkbox("reverse z", &config_.reverseZ)) {
		if (resetReverseZFunc_) {
			resetReverseZFunc_();
		}
	}

	// Anti aliasing
	const char* aaItems[] = {
		"NONE",
		"MSAA",
		"FXAA",
	};
	ImGui::Separator();
	ImGui::Text("Anti-aliasing");
	for (int i = 0; i < 3; i++) {
		if (ImGui::RadioButton(aaItems[i], config_.aaType == i)) {
			config_.aaType = i;
		}
		ImGui::SameLine();
	}
}

更新光源

update()负责更新光源位置,并回调用户更新光源的函数.

void ConfigPanel::update() {
	// update light position
	config_.pointLightPosition = 2.f * glm::vec3(
		glm::sin(lightPositionAngle_),
		1.2f,
		glm::cos(lightPositionAngle_));
	if (updateLightFunc_) {
		updateLightFunc_(config_.pointLightPosition, config_.pointLightColor);
	}
}

ConfigPanel从面板UI读取光源位置角度(滑块)、光源颜色(3个文本输入框),在update中转换为光源位置后,交给用户,根据用户自己的光照模型,对shading point进行着色.

设从滑块读取的光源位置角度\(α\in [0,360)deg\),则

\[light\_position = (2sin α, 2.4, 2cos α) \]

可以看出,光源位置运行轨道是y=2.4的圆.

下面看看update的客户端是什么样的,即回调的是什么函数,客户如何更新光源的.

// ViewManager.h

void ViewManager::setupConfigPanelActions() {
    ...
    // 更新光源时,同步更新场景点光源的属性
    configPanel_->setUpdateLightFunc([&](glm::vec3& position, glm::vec3& color)->void {
        auto& scene = modelLoader_->getScene();
        scene.pointLight.vertexes[0].a_position = position; // 位置
        scene.pointLight.UpdateVertexes(); // 同步GPU数据
        scene.pointLight.material->baseColor = glm::vec4(color, 1.f); // 材质基础颜色
        });
}

传入updateLightFunc_的是一个lambda表达式.

首先,利用ModelLoader对象获取场景Scene对象,然后用Scene设置点光源信息,接着将光源信息同步到GPU,最后将材质的基础颜色修改为光照颜色(update回调传入).

是否需要捕获IO事件

ImGui并不是随时随地都需要捕获鼠标、键盘等IO事件,有可能是其他程序需要捕获的事件. ImGui提供一组状态,用于判断鼠标、键盘等IO输入是否应由ImGui处理.

而ConfigPanel对这些状态用函数进行了包装:

  • wantCaptureKeyboard 检查当前是否需要捕获键盘输入
  • wantCaptureMouse 检查当前是否需要捕获鼠标输入
// 检测ImGui 是否正在捕获键盘输入
// @return io.WantCaptureKeyboard,供外部逻辑判断是否屏蔽键盘输入
// - true:表示 ImGui 当前正在使用键盘输入(例如输入框聚焦、快捷键触发等),此时应禁止游戏/应用的键盘响应
// - false: 表示 ImGui 未占用键盘,游戏/应用可以处理键盘事件
bool ConfigPanel::wantCaptureKeyboard() {
	ImGuiIO& io = ImGui::GetIO();
	return io.WantCaptureKeyboard;
}

// 检测 ImGui 是否正在捕获鼠标输入
bool ConfigPanel::wantCaptureMouse() {
	ImGuiIO& io = ImGui::GetIO();
	return io.WantCaptureMouse;
}

可以看看ConfigPanel::wantCaptureMouse()的客户端,返回true时,直接忽略该事件;返回false,才处理.

// Main.cpp
// glfw: whenever the mouse moves, this callback is called
void mouseCallback(GLFWwindow* window, double xPos, double yPos) {
    if (!viewer || viewer->wantCaptureMouse()) {
        return;
    }
    ...
}

ImGui

使用流程

ImGui使用流程:

  1. 初始化流程(GLFW+OpenGL3)
// 初始化GLFW和OpenGL上下文
glfwInit();
GLFWwindow* window = glfwCreateWindow(...);

// 初始化ImGui
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();

// 绑定后端
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 330"); // 匹配你的GLSL版本

// 设置样式
ImGui::StyleColorsDark();
  1. 主渲染循环
while (!glfwWindowShouldClose(window)) {
    // draw frame use OpenGL
    ...

    // 开始新帧
    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();
    ImGui::NewFrame();

    // 构建UI(示例:显示一个窗口)
    ImGui::Begin("Demo Window");
    ImGui::Text("Hello, OpenGL!");
    if (ImGui::Button("Click Me")) { ... }
    ImGui::End();

    // 渲染
    ImGui::Render();
    glClear(GL_COLOR_BUFFER_BIT);
    
    // 实际绘制ImGui数据
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

    glfwSwapBuffers(window);
}
posted @ 2025-06-19 10:50  明明1109  阅读(59)  评论(0)    收藏  举报