• 【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函数进行渲染,大致伪代码如下:

    1. init() {
    2.      glGenFramebuffers(1, &m_bufferID);  // 生成帧缓存
    3.      glGenTextures(1, &m_renderTexture)  // 生成纹理对象
    4.      // 设置纹理格式
    5.      glBindTexture(GL_TEXTURE_2D, m_renderTexture);
    6.      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    7.      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    8.      glBindTexture(GL_TEXTURE_2D, 0);
    9. // 将纹理作为颜色附件绑定到帧缓存上
    10. glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, m_renderTexture, 0);
    11.      glGenRenderbuffers(1, &m_depthStencilBuffer); // 生成渲染对象
    12. // 设置渲染对象数据格式
    13. glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_STENCIL, p_width, p_height);
    14. // 配置成帧缓存的深度缓冲与模板缓冲附件
    15. glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_depthStencilBuffer);
    16. glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, m_depthStencilBuffer);
    17.   }
    18. display() {
    19. // 1. 绑定帧缓存
    20. glBindFramebuffer(GL_FRAMEBUFFER, m_bufferID);
    21. // 2. 渲染物体到帧缓存
    22. glClearColor();
    23. glClear();
    24. draw();
    25. // 3. 解绑帧缓存
    26. glBindFramebuffer(GL_FRAMEBUFFER, 0);
    27. // 4. 使用帧缓存渲染出来的纹理
    28. ...
    29. glActiveTexture();
    30. glBindTexture(GL_TEXTURE_2D, id);
    31. }

      init函数中的代码基本保持不变。                                       

    二、Overload对FrameBuffer的封装

    Overload将FrameBuffer封装成类Framebuffer,代码位于Framebuffer.h、Framebuffer.cpp中。先看Framebuffer.h文件,Framebuffer类的定义如下,如果对注释中的名词不太熟悉需学习一下OpenGL。

    1. class Framebuffer
    2. {
    3. public:
    4. /**
    5. * 构造函数,会直接创建一个帧缓冲
    6. * @param p_width 帧缓冲的宽
    7. * @param p_height 帧缓存的高
    8. */
    9. Framebuffer(uint16_t p_width = 0, uint16_t p_height = 0);
    10. /**
    11. * 析构函数
    12. */
    13. ~Framebuffer();
    14. /**
    15. * 绑定帧缓冲,对其进行操作
    16. */
    17. void Bind();
    18. /**
    19. * 解除绑定
    20. */
    21. void Unbind();
    22. /**
    23. * 对帧缓冲的大小进行改变
    24. * @param p_width 新的帧缓冲宽度
    25. * @param p_height 新的帧缓冲高度
    26. */
    27. void Resize(uint16_t p_width, uint16_t p_height);
    28. /**
    29. * 帧缓冲的id
    30. */
    31. uint32_t GetID();
    32. /**
    33. * 返回OpenGL纹理附件的id
    34. */
    35. uint32_t GetTextureID();
    36. /**
    37. * 返回渲染缓存的id,这个方法在Overload中其他地方没有使用
    38. */
    39. uint32_t GetRenderBufferID();
    40. private:
    41. uint32_t m_bufferID = 0; // 帧缓冲的id
    42. uint32_t m_renderTexture = 0; // 纹理附件的id
    43. uint32_t m_depthStencilBuffer = 0; // 渲染缓存的id
    44. };

    先来看其构造函数的实现:

    1. OvRendering::Buffers::Framebuffer::Framebuffer(uint16_t p_width, uint16_t p_height)
    2. {
    3. /* Generate OpenGL objects */
    4. glGenFramebuffers(1, &m_bufferID); // 生成帧缓冲id
    5. glGenTextures(1, &m_renderTexture); // 生成颜色缓冲纹理
    6. glGenRenderbuffers(1, &m_depthStencilBuffer); // 生成渲染缓存
    7. // 设置m_renderTexture纹理参数
    8. glBindTexture(GL_TEXTURE_2D, m_renderTexture);
    9. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    10. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    11. glBindTexture(GL_TEXTURE_2D, 0);
    12. /* Setup framebuffer */
    13. Bind();
    14. // 将纹理设置为渲染目标
    15. glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, m_renderTexture, 0);
    16. Unbind();
    17. Resize(p_width, p_height);
    18. }

    构造中直接生成帧缓存、纹理、渲染缓存对象,并将纹理作为颜色附件关联到帧缓存上。再看resize方法。

    1. void OvRendering::Buffers::Framebuffer::Resize(uint16_t p_width, uint16_t p_height)
    2. {
    3. /* Resize texture */
    4. // 设置纹理的大小
    5. glBindTexture(GL_TEXTURE_2D, m_renderTexture);
    6. glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, p_width, p_height, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);
    7. glBindTexture(GL_TEXTURE_2D, 0);
    8. /* Setup depth-stencil buffer (24 + 8 bits) */
    9. glBindRenderbuffer(GL_RENDERBUFFER, m_depthStencilBuffer);
    10. glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_STENCIL, p_width, p_height);
    11. glBindRenderbuffer(GL_RENDERBUFFER, 0);
    12. /* Attach depth and stencil buffer to the framebuffer */
    13. Bind();
    14. // 配置深度缓冲与模板缓冲
    15. glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_depthStencilBuffer);
    16. glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, m_depthStencilBuffer);
    17. Unbind();
    18. }

    这俩方法加起来跟前面的伪代码init函数基本一致,只是用面向对象的方式进行了封装而已。

    三、鼠标拾取原理

    Overload中鼠标拾取是先将物体的id渲染到纹理中,根据鼠标位置读取这张图上的对应的像素值,之后解码获取对象的id。下图红框中是这个函数的关键三个步骤:

     我们先来看RenderSceneForActorPicking这个函数。这个函数是把场景中的物体、摄像机、灯光进行渲染。他们三者的渲染方式很类似,以渲染摄像机为例,代码如下:

    1. /* Render cameras */
    2. for (auto camera : m_context.sceneManager.GetCurrentScene()->GetFastAccessComponents().cameras)
    3. {
    4. auto& actor = camera->owner;
    5. if (actor.IsActive())
    6. {
    7. // 对摄像机的id进行编码,设置到Shader的unfiorm中
    8. PreparePickingMaterial(actor, m_actorPickingMaterial);
    9. auto& model = *m_context.editorResources->GetModel("Camera");
    10. auto modelMatrix = CalculateCameraModelMatrix(actor);
    11. // 绘制摄像机,其覆盖区域的像素全部是其id
    12. m_context.renderer->DrawModelWithSingleMaterial(model, m_actorPickingMaterial, &modelMatrix);
    13. }
    14. }

    这里有一个特殊函数PreparePickingMaterial,将id的三个字节变成颜色保持到u_Diffuse变量中,这个变量Shader中会使用。核心代码见下图红框,这种编码方式是将信息写入图像常用的方式,可以直接拿来借鉴参考。

    要想在FrameBuffer中绘制肯定需要Shader。Overload的Shader是封装成了材料,对于拾取需要特殊的材料,RenderSceneForActorPicking函数中变量m_actorPickingMaterial就保存的这种材料。我们跟踪代码,找这个变量的初始化,可以找到以下代码:

    1. /* Picking Material */
    2. auto unlit = m_context.shaderManager[":Shaders\\Unlit.glsl"];
    3. m_actorPickingMaterial.SetShader(unlit);
    4. m_actorPickingMaterial.Set("u_Diffuse", FVector4(1.f, 1.f, 1.f, 1.0f));
    5. m_actorPickingMaterial.Set("u_DiffuseMap", nullptr);
    6. m_actorPickingMaterial.SetFrontfaceCulling(false);
    7. m_actorPickingMaterial.SetBackfaceCulling(false);

    看来这个Shader是保存在文件Unlit.glsl中的,同时注意u_DiffuseMap设成了null,记住这一点,这是故意为之,魔鬼都隐藏在这些细节当中。

    我们打开这个文件,分析这个Shader:

    1. #shader vertex
    2. #version 430 core
    3. layout (location = 0) in vec3 geo_Pos;
    4. layout (location = 1) in vec2 geo_TexCoords;
    5. layout (location = 2) in vec3 geo_Normal;
    6. layout (std140) uniform EngineUBO
    7. {
    8. mat4 ubo_Model;
    9. mat4 ubo_View;
    10. mat4 ubo_Projection;
    11. vec3 ubo_ViewPos;
    12. float ubo_Time;
    13. };
    14. out VS_OUT
    15. {
    16. vec2 TexCoords;
    17. } vs_out;
    18. void main()
    19. {
    20. vs_out.TexCoords = geo_TexCoords;
    21. gl_Position = ubo_Projection * ubo_View * ubo_Model * vec4(geo_Pos, 1.0);
    22. }
    23. #shader fragment
    24. #version 430 core
    25. out vec4 FRAGMENT_COLOR;
    26. in VS_OUT
    27. {
    28. vec2 TexCoords;
    29. } fs_in;
    30. uniform vec4 u_Diffuse = vec4(1.0, 1.0, 1.0, 1.0);
    31. uniform sampler2D u_DiffuseMap;
    32. uniform vec2 u_TextureTiling = vec2(1.0, 1.0);
    33. uniform vec2 u_TextureOffset = vec2(0.0, 0.0);
    34. void main()
    35. {
    36. 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;
    37. }

    这个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。

    读代码就是要将这些细节看明白,才能照猫画虎,用到我们自己的项目中!

  • 相关阅读:
    npm install 报错解决记录
    es6新增-Generator(异步编程的解决方案2)
    C++(day6)
    Access denied for user ‘root‘@‘localhost‘ (using password: YES)解决方案
    出现 Transaction rolled back because it has been marked as rollback-only 解决方法
    Spark基础【运行架构、RDD】
    计算机结构体系:系统CPI计算例题(1.5)
    【python数学建模】Matplotlib库
    ubuntu server 更改时区:上海
    COM组件IDispatch操作
  • 原文地址:https://blog.csdn.net/loveoobaby/article/details/133583784