【オーバーロードゲームエンジンの詳細解析】エディタオブジェクトのマウスピッキング原理

     Overloadのシーンビューエリアにはピッキングマウス機能があり、選択したオブジェクトをクリックするとインスペクターパネルに表示されます。この記事では、マウス ピックアップ機能の背後にある原理を分析します。

1. OpenGL フレームバッファ

マウス ピッキングを実装するには、テクスチャへの ID のレンダリングとレイ キャスティングの交差という 2 つの一般的な方法があります。オーバーロードはテクスチャへのレンダリング ID を使用し、その実装には OpenGL のフレーム バッファーを使用する必要があるため、最初に OpenGL のフレーム バッファーについて理解する必要があります。

一般に説明するキャッシュは、デフォルトではウィンドウ キャッシュを指し、ウィンドウに直接表示されます。カスタム キャッシュを作成して、GPU パイプラインにテクスチャをレンダリングさせ、このテクスチャを他の場所で使用することもできます。また、テクスチャ内のデータはバイナリ値のみであり、色である必要はなく、任意の意味のあるデータを書き込むことができます。

フレームバッファ オブジェクトを作成したい場合は、glGenFramebuffers() を呼び出して、未使用の識別子を取得する必要があります。フレーム バッファを使用する場合は、最初に glBindFramebuffer(GL_FRAMEBUFFER,bufferID) バインディングを呼び出す必要があります。テクスチャ マップにレンダリングする場合は、glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENTi, textureId, level) を呼び出して、テクスチャ マップのレベル level をフレーム バッファ アタッチメントに関連付ける必要があります。レンダリングに深度キャッシュとテンプレート キャッシュも必要な場合は、レンダリング キャッシュも必要になります。

レンダリング キャッシュは、OpenGL によって管理される効率的なメモリ領域でもあり、データを特定の形式で保存でき、フレーム バッファーに関連付けられている場合にのみ意味を持ちます。glGenRenderbuffers を呼び出すとレンダリング バッファーを作成できますが、それを操作する際にはバインド操作も必要です。バインド時に glBindRenderbuffer を使用します。

これを見て、フレーム バッファーは複雑すぎて使用できないと思いませんか? 実はフレームバッファの設定は全て固定形式のコードであり、ルーチンも基本的に同じなので、まずは擬似コードで繋ぎ合わせてみましょう。プログラムがプロセス用に設計されていると仮定すると、最初に初期化のために init 関数を呼び出し、次にメイン ループでレンダリングのために表示関数を継続的に呼び出します。おおよその疑似コードは次のとおりです。

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 関数のコードは基本的に同じままです。                                       

2. オーバーロードによる FrameBuffer のカプセル化

オーバーロードは 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; // 帧缓冲的id
		uint32_t m_renderTexture = 0; // 纹理附件的id
		uint32_t m_depthStencilBuffer = 0; // 渲染缓存的id
	};

まずコンストラクターの実装を見てみましょう。

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); // 生成渲染缓存

	// 设置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);
}

フレーム バッファ、テクスチャ、レンダリング キャッシュ オブジェクトはコンストラクション内で直接生成され、テクスチャはカラー アタッチメントとしてフレーム バッファに関連付けられます。サイズ変更方法をもう一度見てみましょう。

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();
}

これら 2 つのメソッドを合計したものは、基本的に前の疑似コード init 関数と同じですが、オブジェクト指向の方法でカプセル化されています。

3. マウスピッキングの原理

オーバーロードでのマウス ピッキングでは、最初にオブジェクトの ID がテクスチャにレンダリングされ、マウスの位置に基づいて画像上の対応するピクセル値が読み取られ、それからデコードされてオブジェクトの ID が取得されます。以下の図の赤いボックスには、この関数の 3 つの主要なステップが示されています。

 まず、RenderSceneForActorPicking 関数を見てみましょう。この関数は、シーン内のオブジェクト、カメラ、ライトをレンダリングします。3 つのレンダリング方法は非常に似ており、レンダリング カメラを例にとると、コードは次のようになります。

	/* 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);
            // 绘制摄像机,其覆盖区域的像素全部是其id
			m_context.renderer->DrawModelWithSingleMaterial(model, m_actorPickingMaterial, &modelMatrix);
		}
	}

ID の 3 バイトを色に変換して u_Diffuse 変数に保存する特別な関数 PreparePickingmaterial があり、この変数はシェーダーで使用されます。コア コードは、下の図の赤枠で示されています。このエンコード方法は、画像に情報を書き込むために一般的に使用される方法であり、参照として直接使用できます。

FrameBufferに描画するにはシェーダーが必ず必要になります。オーバーロードのシェーダーはマテリアルにカプセル化されています。ピッキングには特別なマテリアルが必要です。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 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;
}

この GPU プログラムの頂点シェーダーについては特に説明する必要はなく、グリッドの NDC 座標を計算するだけで完了です。不可解なのは Fragment Shader の最後のコード行ですが、結論から先に言いますと、このコード行は FRAGMENT_COLOR = u_Diffuse に相当します。理由としては、簡単に言うと、アプリではu_DiffuseMapがnullに設定されているのですが、CPUに渡す際にnull値のテクスチャが空のテクスチャに設定されてしまうのです。この空のテクスチャのサイズは 1 ピクセルで、その値は真っ白なので、サンプリング結果はすべて 1.0 です。

空のコンテキストの初期化については、次のコードを参照してください。

 ピクセルが 1 つだけあり、値が 1.0 であるかどうかを確認します。

とはいえ、ピッキングに必要なテクスチャ レンダリングの核となる部分は基本的に完成しました。このテクスチャを読み取る方法を見てみましょう。

まず以下のマウス位置を取得します。imguiで描画しているのでマウスの絶対位置を画像の相対位置に変換する必要があります。最初に FrameBuffer をバインドし、glReadPixels を使用してピクセルを読み取ります。画像形式は RGB であることに注意してください。これは、FrameBuffer の初期化の設定と一致しています。これらの詳細に注意する必要があり、多くの謎があります。最後に、ピクセルがデコードされてシーン オブジェクトの ID が取得されます。

コードを読むということは、これらの詳細を理解し、模倣して独自のプロジェクトで使用できるようにすることを意味します。

おすすめ

転載: blog.csdn.net/loveoobaby/article/details/133583784