? ? ?Overload的場(chǎng)景視圖區(qū)有拾取鼠標(biāo)功能,單擊拾取物體后會(huì)顯示在Inspector面板中。本文來分析鼠標(biāo)拾取這個(gè)功能背后的原理。
一、OpenGL的FrameBuffer
實(shí)現(xiàn)鼠標(biāo)拾取常用的方式有兩種:渲染id到紋理、光線投射求交。Overload使用的是渲染id到紋理,其實(shí)現(xiàn)需借助OpenGL的幀緩沖FrameBuffer,所以要先了解一下OpenGL的幀緩沖。
我們一般討論的緩存默認(rèn)指窗口緩存,直接顯示在窗口中。我們也可以創(chuàng)建一個(gè)自定義的緩存,讓GPU管線渲染到紋理當(dāng)中,之后在其他地方可以使用這張紋理。并且紋理中的數(shù)據(jù)只是二進(jìn)制值,不一定非得是顏色,可以寫入任意有意義的數(shù)據(jù)。
如果我們要?jiǎng)?chuàng)建幀緩存對(duì)象,需要調(diào)用glGenFramebuffers(),得到一個(gè)未使用的標(biāo)識(shí)符。在使用幀緩存的時(shí)候需要先調(diào)用glBindFramebuffer(GL_FRAMEBUFFER, bufferID)綁定。如果要渲染到紋理貼圖,需調(diào)用glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENTi, textureId, level)將紋理貼圖的level層級(jí)關(guān)聯(lián)到幀緩存附件上。如果渲染還需要深度緩存、模板緩存那么還需要渲染緩存。
渲染緩存同樣也是OpenGL所管理的一處高效內(nèi)存區(qū)域,它可以存儲(chǔ)特定格式的數(shù)據(jù),其只有關(guān)聯(lián)到一個(gè)幀緩存才有意義。調(diào)用glGenRenderbuffers可以創(chuàng)建渲染緩存,操作它的時(shí)候同樣需要綁定操作。綁定的時(shí)候使用glBindRenderbuffer。
看到這里是不是覺得幀緩存使用起來太復(fù)雜了?其實(shí)幀緩存的設(shè)置都是固定格式的代碼,套路基本一樣,先用偽代碼串一下。假設(shè)我們的程序是面向過程設(shè)計(jì)的,先用調(diào)用init函數(shù)進(jìn)行初始化,之后主循環(huán)不斷調(diào)用display函數(shù)進(jìn)行渲染,大致偽代碼如下:
init() {
? ? ?glGenFramebuffers(1, &m_bufferID);? // 生成幀緩存
? ? ?glGenTextures(1, &m_renderTexture)? // 生成紋理對(duì)象
?????// 設(shè)置紋理格式
?????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); // 生成渲染對(duì)象
// 設(shè)置渲染對(duì)象數(shù)據(jù)格式
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函數(shù)中的代碼基本保持不變。? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
二、Overload對(duì)FrameBuffer的封裝
Overload將FrameBuffer封裝成類Framebuffer,代碼位于Framebuffer.h、Framebuffer.cpp中。先看Framebuffer.h文件,F(xiàn)ramebuffer類的定義如下,如果對(duì)注釋中的名詞不太熟悉需學(xué)習(xí)一下OpenGL。
class Framebuffer
{
public:
/**
* 構(gòu)造函數(shù),會(huì)直接創(chuàng)建一個(gè)幀緩沖
* @param p_width 幀緩沖的寬
* @param p_height 幀緩存的高
*/
Framebuffer(uint16_t p_width = 0, uint16_t p_height = 0);
/**
* 析構(gòu)函數(shù)
*/
~Framebuffer();
/**
* 綁定幀緩沖,對(duì)其進(jìn)行操作
*/
void Bind();
/**
* 解除綁定
*/
void Unbind();
/**
* 對(duì)幀緩沖的大小進(jìn)行改變
* @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,這個(gè)方法在Overload中其他地方?jīng)]有使用
*/
uint32_t GetRenderBufferID();
private:
uint32_t m_bufferID = 0; // 幀緩沖的id
uint32_t m_renderTexture = 0; // 紋理附件的id
uint32_t m_depthStencilBuffer = 0; // 渲染緩存的id
};
先來看其構(gòu)造函數(shù)的實(shí)現(xiàn):
OvRendering::Buffers::Framebuffer::Framebuffer(uint16_t p_width, uint16_t p_height)
{
/* Generate OpenGL objects */
glGenFramebuffers(1, &m_bufferID); // 生成幀緩沖id
glGenTextures(1, &m_renderTexture); // 生成顏色緩沖紋理
glGenRenderbuffers(1, &m_depthStencilBuffer); // 生成渲染緩存
// 設(shè)置m_renderTexture紋理參數(shù)
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();
// 將紋理設(shè)置為渲染目標(biāo)
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, m_renderTexture, 0);
Unbind();
Resize(p_width, p_height);
}
構(gòu)造中直接生成幀緩存、紋理、渲染緩存對(duì)象,并將紋理作為顏色附件關(guān)聯(lián)到幀緩存上。再看resize方法。
void OvRendering::Buffers::Framebuffer::Resize(uint16_t p_width, uint16_t p_height)
{
/* Resize texture */
// 設(shè)置紋理的大小
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函數(shù)基本一致,只是用面向?qū)ο蟮姆绞竭M(jìn)行了封裝而已。
三、鼠標(biāo)拾取原理
Overload中鼠標(biāo)拾取是先將物體的id渲染到紋理中,根據(jù)鼠標(biāo)位置讀取這張圖上的對(duì)應(yīng)的像素值,之后解碼獲取對(duì)象的id。下圖紅框中是這個(gè)函數(shù)的關(guān)鍵三個(gè)步驟:
?我們先來看RenderSceneForActorPicking這個(gè)函數(shù)。這個(gè)函數(shù)是把場(chǎng)景中的物體、攝像機(jī)、燈光進(jìn)行渲染。他們?nèi)叩匿秩痉绞胶茴愃?,以渲染攝像機(jī)為例,代碼如下:
/* Render cameras */
for (auto camera : m_context.sceneManager.GetCurrentScene()->GetFastAccessComponents().cameras)
{
auto& actor = camera->owner;
if (actor.IsActive())
{
// 對(duì)攝像機(jī)的id進(jìn)行編碼,設(shè)置到Shader的unfiorm中
PreparePickingMaterial(actor, m_actorPickingMaterial);
auto& model = *m_context.editorResources->GetModel("Camera");
auto modelMatrix = CalculateCameraModelMatrix(actor);
// 繪制攝像機(jī),其覆蓋區(qū)域的像素全部是其id
m_context.renderer->DrawModelWithSingleMaterial(model, m_actorPickingMaterial, &modelMatrix);
}
}
這里有一個(gè)特殊函數(shù)PreparePickingMaterial,將id的三個(gè)字節(jié)變成顏色保持到u_Diffuse變量中,這個(gè)變量Shader中會(huì)使用。核心代碼見下圖紅框,這種編碼方式是將信息寫入圖像常用的方式,可以直接拿來借鑒參考。
要想在FrameBuffer中繪制肯定需要Shader。Overload的Shader是封裝成了材料,對(duì)于拾取需要特殊的材料,RenderSceneForActorPicking函數(shù)中變量m_actorPickingMaterial就保存的這種材料。我們跟蹤代碼,找這個(gè)變量的初始化,可以找到以下代碼:
/* 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);
看來這個(gè)Shader是保存在文件Unlit.glsl中的,同時(shí)注意u_DiffuseMap設(shè)成了null,記住這一點(diǎn),這是故意為之,魔鬼都隱藏在這些細(xì)節(jié)當(dāng)中。
我們打開這個(gè)文件,分析這個(gè)Shader:
#shader vertex
#version 430 core
layout (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 core
out 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;
}
這個(gè)GPU程序的Vertex Shader沒啥可說的,計(jì)算一下網(wǎng)格的NDC坐標(biāo)完事。令人費(fèi)解的是Fragment Shader的最后一行代碼,我這里先說結(jié)論,這行代碼等價(jià)于FRAGMENT_COLOR =?u_Diffuse。?至于為什么,簡(jiǎn)單來說應(yīng)用程序中將u_DiffuseMap設(shè)成了null,但傳給CPU的時(shí)候會(huì)將值是null的紋理設(shè)置成空紋理。這個(gè)空紋理大小一個(gè)像素,值是純白色,那么對(duì)其采樣結(jié)果都是1.0 。
空文理初始化見以下代碼:
?看看是不是只有一個(gè)像素,而且值都是1.0。
說道這里,拾取需要的紋理渲染核心細(xì)節(jié)基本說完了。我們?cè)賮砜纯慈绾巫x取這個(gè)紋理的。
先獲取以下鼠標(biāo)位置。由于是用imgui繪制的,需要對(duì)鼠標(biāo)的絕對(duì)位置變換到圖像的相對(duì)位置上。 先綁定FrameBuffer,使用glReadPixels讀取像素,注意圖片格式是RGB,跟初始化FrameBuffer進(jìn)行的設(shè)置一致,這些細(xì)節(jié)都得注意,玄機(jī)很多。最后對(duì)像素進(jìn)行解碼操作獲取場(chǎng)景物體的id。文章來源:http://www.zghlxwxcb.cn/news/detail-765823.html
讀代碼就是要將這些細(xì)節(jié)看明白,才能照貓畫虎,用到我們自己的項(xiàng)目中!文章來源地址http://www.zghlxwxcb.cn/news/detail-765823.html
到了這里,關(guān)于【Overload游戲引擎細(xì)節(jié)分析】編輯器對(duì)象鼠標(biāo)拾取原理的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!