【Overload游戏引擎分析】编辑器对象鼠标拾取原理
Overload的场景视图区有拾取鼠标功能,单击拾取物体后会显示在Inspector面板中。本文来分析鼠标拾取这个功能背后的原理。

一、OpenGL的FrameBuffer
实现鼠标拾取常用的方式有两种:渲染id到纹理、光线投射求交。Overload使用的是渲染id到纹理,其实现需借助OpenGL的帧缓冲FrameBuffer,所以要先了解一下OpenGL的帧缓冲。
我们一般讨论的缓存默认指窗口缓存,直接显示在窗口中。我们也可以创建一个自定义的缓存,让GPU管线渲染到纹理当中,之后在其他地方可以使用这张纹理。并且纹理中的数据只是二进制值,不一定非得是颜色,可以写入任意有意义的数据。
如果我们要创建帧缓存对象,需要调用glGenFramebuffers(),得到一个未使用的标识符。在使用帧缓存的时候需要先调用glBindFramebuffer(GL_FRAMEBUFFER, bufferID)绑定。如果要渲染到纹理贴图,需调用glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENTi, textureId, level)将纹理贴图的level层级关联到帧缓存附件上。如果渲染还需要深度缓存、模板缓存那么还需要渲染缓存。
渲染缓存同样也是OpenGL所管理的一处高效内存区域,它可以存储特定格式的数据,其只有关联到一个帧缓存才有意义。调用glGenRenderbuffers可以创建渲染缓存,操作它的时候同样需要绑定操作。绑定的时候使用glBindRenderbuffer。
看到这里是不是觉得帧缓存使用起来太复杂了?其实帧缓存的设置都是固定格式的代码,套路基本一样,先用伪代码串一下。假设我们的程序是面向过程设计的,先用调用init函数进行初始化,之后主循环不断调用display函数进行渲染,大致伪代码如下:
init() {glGenFramebuffers(1, &m_bufferID); // 生成帧缓存glGenTextures(1, &m_renderTexture) // 生成纹理对象// 设置纹理格式glBindTexture(GL_TEXTURE_2D, m_renderTexture);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);glBindTexture(GL_TEXTURE_2D, 0);// 将纹理作为颜色附件绑定到帧缓存上glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, m_renderTexture, 0);glGenRenderbuffers(1, &m_depthStencilBuffer); // 生成渲染对象// 设置渲染对象数据格式glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_STENCIL, p_width, p_height);// 配置成帧缓存的深度缓冲与模板缓冲附件glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_depthStencilBuffer);glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, m_depthStencilBuffer);}display() {// 1. 绑定帧缓存glBindFramebuffer(GL_FRAMEBUFFER, m_bufferID);// 2. 渲染物体到帧缓存glClearColor();glClear();draw();// 3. 解绑帧缓存glBindFramebuffer(GL_FRAMEBUFFER, 0);// 4. 使用帧缓存渲染出来的纹理...glActiveTexture();glBindTexture(GL_TEXTURE_2D, id);}
init函数中的代码基本保持不变。
二、Overload对FrameBuffer的封装
Overload将FrameBuffer封装成类Framebuffer,代码位于Framebuffer.h、Framebuffer.cpp中。先看Framebuffer.h文件,Framebuffer类的定义如下,如果对注释中的名词不太熟悉需学习一下OpenGL。
class Framebuffer{public:/*** 构造函数,会直接创建一个帧缓冲* @param p_width 帧缓冲的宽* @param p_height 帧缓存的高*/Framebuffer(uint16_t p_width = 0, uint16_t p_height = 0);/*** 析构函数*/~Framebuffer();/*** 绑定帧缓冲,对其进行操作*/void Bind();/*** 解除绑定*/void Unbind();/*** 对帧缓冲的大小进行改变* @param p_width 新的帧缓冲宽度* @param p_height 新的帧缓冲高度*/void Resize(uint16_t p_width, uint16_t p_height);/*** 帧缓冲的id*/uint32_t GetID();/*** 返回OpenGL纹理附件的id*/uint32_t GetTextureID();/*** 返回渲染缓存的id,这个方法在Overload中其他地方没有使用*/uint32_t GetRenderBufferID();private:uint32_t m_bufferID = 0; // 帧缓冲的iduint32_t m_renderTexture = 0; // 纹理附件的iduint32_t m_depthStencilBuffer = 0; // 渲染缓存的id};
先来看其构造函数的实现:
OvRendering::Buffers::Framebuffer::Framebuffer(uint16_t p_width, uint16_t p_height)
{/* Generate OpenGL objects */glGenFramebuffers(1, &m_bufferID); // 生成帧缓冲idglGenTextures(1, &m_renderTexture); // 生成颜色缓冲纹理glGenRenderbuffers(1, &m_depthStencilBuffer); // 生成渲染缓存// 设置m_renderTexture纹理参数glBindTexture(GL_TEXTURE_2D, m_renderTexture);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);glBindTexture(GL_TEXTURE_2D, 0);/* Setup framebuffer */Bind();// 将纹理设置为渲染目标glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, m_renderTexture, 0);Unbind();Resize(p_width, p_height);
}
构造中直接生成帧缓存、纹理、渲染缓存对象,并将纹理作为颜色附件关联到帧缓存上。再看resize方法。
void OvRendering::Buffers::Framebuffer::Resize(uint16_t p_width, uint16_t p_height)
{/* Resize texture */// 设置纹理的大小glBindTexture(GL_TEXTURE_2D, m_renderTexture);glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, p_width, p_height, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);glBindTexture(GL_TEXTURE_2D, 0);/* Setup depth-stencil buffer (24 + 8 bits) */glBindRenderbuffer(GL_RENDERBUFFER, m_depthStencilBuffer);glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_STENCIL, p_width, p_height);glBindRenderbuffer(GL_RENDERBUFFER, 0);/* Attach depth and stencil buffer to the framebuffer */Bind();// 配置深度缓冲与模板缓冲glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_depthStencilBuffer);glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, m_depthStencilBuffer);Unbind();
}
这俩方法加起来跟前面的伪代码init函数基本一致,只是用面向对象的方式进行了封装而已。
三、鼠标拾取原理
Overload中鼠标拾取是先将物体的id渲染到纹理中,根据鼠标位置读取这张图上的对应的像素值,之后解码获取对象的id。下图红框中是这个函数的关键三个步骤:

我们先来看RenderSceneForActorPicking这个函数。这个函数是把场景中的物体、摄像机、灯光进行渲染。他们三者的渲染方式很类似,以渲染摄像机为例,代码如下:
/* Render cameras */for (auto camera : m_context.sceneManager.GetCurrentScene()->GetFastAccessComponents().cameras){auto& actor = camera->owner;if (actor.IsActive()){// 对摄像机的id进行编码,设置到Shader的unfiorm中PreparePickingMaterial(actor, m_actorPickingMaterial);auto& model = *m_context.editorResources->GetModel("Camera");auto modelMatrix = CalculateCameraModelMatrix(actor);// 绘制摄像机,其覆盖区域的像素全部是其idm_context.renderer->DrawModelWithSingleMaterial(model, m_actorPickingMaterial, &modelMatrix);}}
这里有一个特殊函数PreparePickingMaterial,将id的三个字节变成颜色保持到u_Diffuse变量中,这个变量Shader中会使用。核心代码见下图红框,这种编码方式是将信息写入图像常用的方式,可以直接拿来借鉴参考。

要想在FrameBuffer中绘制肯定需要Shader。Overload的Shader是封装成了材料,对于拾取需要特殊的材料,RenderSceneForActorPicking函数中变量m_actorPickingMaterial就保存的这种材料。我们跟踪代码,找这个变量的初始化,可以找到以下代码:
/* Picking Material */
auto unlit = m_context.shaderManager[":Shaders\\Unlit.glsl"];
m_actorPickingMaterial.SetShader(unlit);
m_actorPickingMaterial.Set("u_Diffuse", FVector4(1.f, 1.f, 1.f, 1.0f));
m_actorPickingMaterial.Set<OvRendering::Resources::Texture*>("u_DiffuseMap", nullptr);
m_actorPickingMaterial.SetFrontfaceCulling(false);
m_actorPickingMaterial.SetBackfaceCulling(false);
看来这个Shader是保存在文件Unlit.glsl中的,同时注意u_DiffuseMap设成了null,记住这一点,这是故意为之,魔鬼都隐藏在这些细节当中。
我们打开这个文件,分析这个Shader:
#shader vertex
#version 430 corelayout (location = 0) in vec3 geo_Pos;
layout (location = 1) in vec2 geo_TexCoords;
layout (location = 2) in vec3 geo_Normal;layout (std140) uniform EngineUBO
{mat4 ubo_Model;mat4 ubo_View;mat4 ubo_Projection;vec3 ubo_ViewPos;float ubo_Time;
};out VS_OUT
{vec2 TexCoords;
} vs_out;void main()
{vs_out.TexCoords = geo_TexCoords;gl_Position = ubo_Projection * ubo_View * ubo_Model * vec4(geo_Pos, 1.0);
}#shader fragment
#version 430 coreout vec4 FRAGMENT_COLOR;in VS_OUT
{vec2 TexCoords;
} fs_in;uniform vec4 u_Diffuse = vec4(1.0, 1.0, 1.0, 1.0);
uniform sampler2D u_DiffuseMap;
uniform vec2 u_TextureTiling = vec2(1.0, 1.0);
uniform vec2 u_TextureOffset = vec2(0.0, 0.0);void main()
{FRAGMENT_COLOR = texture(u_DiffuseMap, u_TextureOffset + vec2(mod(fs_in.TexCoords.x * u_TextureTiling.x, 1), mod(fs_in.TexCoords.y * u_TextureTiling.y, 1))) * u_Diffuse;
}
这个GPU程序的Vertex Shader没啥可说的,计算一下网格的NDC坐标完事。令人费解的是Fragment Shader的最后一行代码,我这里先说结论,这行代码等价于FRAGMENT_COLOR = u_Diffuse。 至于为什么,简单来说应用程序中将u_DiffuseMap设成了null,但传给CPU的时候会将值是null的纹理设置成空纹理。这个空纹理大小一个像素,值是纯白色,那么对其采样结果都是1.0 。
空文理初始化见以下代码:

看看是不是只有一个像素,而且值都是1.0。
说道这里,拾取需要的纹理渲染核心细节基本说完了。我们再来看看如何读取这个纹理的。

先获取以下鼠标位置。由于是用imgui绘制的,需要对鼠标的绝对位置变换到图像的相对位置上。 先绑定FrameBuffer,使用glReadPixels读取像素,注意图片格式是RGB,跟初始化FrameBuffer进行的设置一致,这些细节都得注意,玄机很多。最后对像素进行解码操作获取场景物体的id。
读代码就是要将这些细节看明白,才能照猫画虎,用到我们自己的项目中!
相关文章:
【Overload游戏引擎分析】编辑器对象鼠标拾取原理
Overload的场景视图区有拾取鼠标功能,单击拾取物体后会显示在Inspector面板中。本文来分析鼠标拾取这个功能背后的原理。 一、OpenGL的FrameBuffer 实现鼠标拾取常用的方式有两种:渲染id到纹理、光线投射求交。Overload使用的是渲染id到纹理,…...
【Spring内容进阶 | 第三篇】AOP进阶内容
前言: 在前面我们已经粗略的介绍了什么是AOP以及各种基础知识点,而本篇我们将聚焦于AOP的细节,详细的讲解一下AOP中的通知类型,通知顺序,切入点表达式以及连接点。通过对AOP的熟练掌握,我们可以快速编写出低…...
华为云ModelArts:引领AI艺术创作的未来,让人人都可以成为“艺术家”!
随着科技的飞速发展,艺术创作逐渐告别传统的画布和画笔,开始走向数字化、智能化的新时代。在这个蓬勃发展的领域中,华为云ModelArts以其强大的功能和出色的性能引领着AI艺术创作的未来。 华为云ModelArts是面向开发者的一站式AI开发平台,为机器学习与深度学习提供海量数据预处…...
Elasticsearch:如何从 Elasticsearch 集群中删除数据节点
Elasticsearch 集群通常包含多个节点,并且可能存在需要从集群中删除节点的情况。 应谨慎执行此过程,以确保数据的完整性和可用性。 在本文中,我们将引导你完成从 Elasticsearch 集群安全删除节点的步骤。 确保集群是绿色的 在尝试从 Elastic…...
长假回归,回顾一下所有的电商API接口
淘宝API接口 item_get 获得淘宝商品详情item_get_pro 获得淘宝商品详情高级版item_review 获得淘宝商品评论 获取测试keyitem_fee 获得淘宝商品快递费用item_password 获得淘口令真实urlitem_list_updown 批量获得淘宝商品上下架时间seller_info 获得淘宝店铺详情item_search…...
认识计算机主板
目录 定义主要部件简单图示 主要功能 定义 计算机主板(Motherboard)是计算机系统中的核心组件之一,也被称为系统板、主板或母板。它是一个电子电路板,用于连接和支持计算机的各种硬件组件,包括中央处理器(…...
PHP乱七八糟面试题
1、请解释PHP中的JWT是什么? JWT(JSON Web Token)是一种用于认证和授权的标准,可以在不同的系统之间安全地传递信息。 在PHP中,可以使用各种JWT库来生成和解析JWT,JWT包含了一些元数据和签名, …...
pom管理规范
0. 引言 在单机架构下,我们只需要将我们的依赖在pom中引入。但是过渡到微服务架构后,会涉及到多模块引用相同的依赖,多模版之间依赖的版本太过分散难以管理的问题。 这就需要我们利用maven中依赖传递的特性,结合dependencyManage…...
AI大模型的安全隐患问题与新兴Anthropic新势力涌动
引言: 无论从社会层面或技术层面,大模型的安全隐患都是一个不容小觑的话题。也正因此,ChatGPT 初兴起时,国内的 To C 大模型产品一时受阻。而尽管 9 月初第一批 8 家大模型通过备案,各家厂商对大模型的安全问题也不敢…...
slamplay:用C++实现的SLAM工具集
0. 项目简介 slamplay 是一个功能强大的工具集合,可用于开始使用 C 来玩和试验 SLAM。这是一项正在进行的工作。它在单个 cmake 框架中安装并提供一些最重要的功能 后端框架(g2o、gtsam、ceres、se-sync 等)、 前端工具(opencv、…...
IPT2602协议-USB 快速充电端口控制器
产品描述: IPT2602是一款USB端口快速充电协议控制芯片。IPT2602智能识别多种快速充电协议,对手机等受电设备进行快速充电。IPT2602根据受电设备发送的电压请求能够精确的调整VBUS输出电压,从而实现快速充电。 IPT2602在调整5V输出电压前会自动…...
Zotero 超好用插件的下载链接及配置方法(PDF-translate/ZotFile/茉莉花/Zotero Scihub)
目录 前言插件安装方法插件一:文献翻译插件(pdf-translate)插件二:文献附件管理(ZotFile)插件三:中文文献插件(茉莉花)插件四:Sci-Hub 自动下载文献ÿ…...
Titus网关中的缓存一致性机制
API网关引入缓存可以在不影响数据一致性的前提下,有效优化接口时延。本文介绍了Netflix在Titus网关上引入缓存的实践,比较了有无缓存对访问时延的影响。原文: Consistent caching mechanism in Titus Gateway 前言 Titus是Netflix的云容器运行时…...
flutter开发实战 - inappwebview设置cookie
flutter开发实战-inappwebview设置cookie 在使用inappwebview时候,需要设置cookie,这里记录一下 一、在initialUserScripts中设置cookie 在inappwebview中有一个initialUserScripts,可以初始化设置cookie等,我们可以通过该属性…...
零基础如何自学网络安全,基于就业前景全方位讲解,包教包会
你是否对网络空间安全充满好奇?想要解开网络世界神秘的面纱?你是否对黑客技术着迷?而找不到合适的学习途径?你是否遭到过各种各样的网络攻击,却因知识的匮乏束手无策? 那么接下来将为你全面介绍,…...
Java项目防止SQL注入的几种方案
目录 一、什么是SQL注入? 二、Java项目防止SQL注入方式 1、PreparedStatement防止SQL注入 2、mybatis中#{}防止SQL注入 3、对请求参数的敏感词汇进行过滤 4、nginx反向代理防止SQL注入 一、什么是SQL注入? SQL注入即是指web应用程序对用户输入数…...
Win11 安装安卓子系统方法教程
WIN11安装安卓子系统 准备工作下载安装安装完成使用adb连接子系统结束 准备工作 开启电脑中的 控制面板>>>>程序和功能>>启用或关闭Windows功能>>>找到“Hyper-V”,把勾都勾上,确定,完成安装,并重启电…...
golang pg 数据库不存在 就创建 --chatPGT
问:linkOrCreateDatabase(addr ), 函数执行 连接 pg数据库,若数据库 不存在就创建 gpt: 要在 Go 中连接到 PostgreSQL 数据库并在数据库不存在时创建数据库,你可以使用 github.com/lib/pq 包以及 database/sql 包。以下是一个示例࿱…...
VUE3照本宣科——eslint与prettier
VUE3照本宣科——eslint与prettier VUE3照本宣科系列导航 前言一、eslint1.配置文件2.配置规则3.忽略错误 二、prettier三、总结 VUE3照本宣科系列导航 1.VUE3照本宣科——认识VUE3 2.VUE3照本宣科——应用实例API与setup 3.VUE3照本宣科——响应式与生命周期钩子 4.VUE3照本宣…...
【谷粒学院】Maven加载问题
问题 maven加载项目时候,默认不会加载src-java文件夹里面xml类型文件的 解决方案 直接赋值xml文件到target目录通过配置实现 (1)在pom.xml文件中配置 <!-- 项目打包时会将java目录中的*.xml文件也进行打包 --> <build><re…...
SpringBoot-17-MyBatis动态SQL标签之常用标签
文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…...
【Linux】shell脚本忽略错误继续执行
在 shell 脚本中,可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行,可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令,并忽略错误 rm somefile…...
Spark 之 入门讲解详细版(1)
1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室(Algorithms, Machines, and People Lab)开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目,8个月后成为Apache顶级项目,速度之快足见过人之处&…...
Day131 | 灵神 | 回溯算法 | 子集型 子集
Day131 | 灵神 | 回溯算法 | 子集型 子集 78.子集 78. 子集 - 力扣(LeetCode) 思路: 笔者写过很多次这道题了,不想写题解了,大家看灵神讲解吧 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili 完…...
成都鼎讯硬核科技!雷达目标与干扰模拟器,以卓越性能制胜电磁频谱战
在现代战争中,电磁频谱已成为继陆、海、空、天之后的 “第五维战场”,雷达作为电磁频谱领域的关键装备,其干扰与抗干扰能力的较量,直接影响着战争的胜负走向。由成都鼎讯科技匠心打造的雷达目标与干扰模拟器,凭借数字射…...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...
【JVM面试篇】高频八股汇总——类加载和类加载器
目录 1. 讲一下类加载过程? 2. Java创建对象的过程? 3. 对象的生命周期? 4. 类加载器有哪些? 5. 双亲委派模型的作用(好处)? 6. 讲一下类的加载和双亲委派原则? 7. 双亲委派模…...
免费数学几何作图web平台
光锐软件免费数学工具,maths,数学制图,数学作图,几何作图,几何,AR开发,AR教育,增强现实,软件公司,XR,MR,VR,虚拟仿真,虚拟现实,混合现实,教育科技产品,职业模拟培训,高保真VR场景,结构互动课件,元宇宙http://xaglare.c…...
Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)
引言 在人工智能飞速发展的今天,大语言模型(Large Language Models, LLMs)已成为技术领域的焦点。从智能写作到代码生成,LLM 的应用场景不断扩展,深刻改变了我们的工作和生活方式。然而,理解这些模型的内部…...
