【Overload游戏引擎分析】画场景网格的Shader

Overload引擎地址: GitHub - adriengivry/Overload: 3D Game engine with editor

一、栅格绘制基本原理

Overload Editor启动之后,场景视图中有栅格线,这个在很多软件中都有。刚开始我猜测它应该是通过绘制线实现的。阅读代码发现,这个栅格的几何网格只有两个三角形面片组成的正方形,使用特殊Shader绘制出来的。

绘制栅格的代码在EditorRenderer.cpp中,代码如下:

cpp 复制代码
void OvEditor::Core::EditorRenderer::RenderGrid(const OvMaths::FVector3& p_viewPos, const OvMaths::FVector3& p_color)
{
    constexpr float gridSize = 5000.0f; // 栅格的总的大小
 
    FMatrix4 model = FMatrix4::Translation({ p_viewPos.x, 0.0f, p_viewPos.z }) * FMatrix4::Scaling({ gridSize * 2.0f, 1.f, gridSize * 2.0f }); // 栅格的模型矩阵
	m_gridMaterial.Set("u_Color", p_color); // 栅格的颜色
	m_context.renderer->DrawModelWithSingleMaterial(*m_context.editorResources->GetModel("Plane"), m_gridMaterial, &model); // 绘制栅格

    // 绘制坐标轴的三条线
    m_context.shapeDrawer->DrawLine(OvMaths::FVector3(-gridSize + p_viewPos.x, 0.0f, 0.0f), OvMaths::FVector3(gridSize + p_viewPos.x, 0.0f, 0.0f), OvMaths::FVector3(1.0f, 0.0f, 0.0f), 1.0f);
    m_context.shapeDrawer->DrawLine(OvMaths::FVector3(0.0f, -gridSize + p_viewPos.y, 0.0f), OvMaths::FVector3(0.0f, gridSize + p_viewPos.y, 0.0f), OvMaths::FVector3(0.0f, 1.0f, 0.0f), 1.0f);
    m_context.shapeDrawer->DrawLine(OvMaths::FVector3(0.0f, 0.0f, -gridSize + p_viewPos.z), OvMaths::FVector3(0.0f, 0.0f, gridSize + p_viewPos.z), OvMaths::FVector3(0.0f, 0.0f, 1.0f), 1.0f);
}

从中看出,先将面片平移到视点的前方,使得三角形始终在视锥体范围内,同时将三角形进行缩放,总的尺寸缩放到10000。然后使用m_gridMaterial材质进行绘制。所谓的材质就是Shader的封装。最后再绘制坐标轴的三条线。

可以使用RenderDoc抓帧,可以验证确实是这么实现的。

二、栅格绘制的Shader代码

绘制栅格的Vertex Shader代码如下:

cpp 复制代码
#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
{
    vec3 FragPos;
    vec2 TexCoords;
} vs_out;

void main()
{
    vs_out.FragPos      = vec3(ubo_Model * vec4(geo_Pos, 1.0)); // 计算顶点世界坐标系坐标
    vs_out.TexCoords    = vs_out.FragPos.xz;  // 对应的纹理坐标,取对应的世界坐标

    gl_Position = ubo_Projection * ubo_View * vec4(vs_out.FragPos, 1.0); // 计算NDC坐标
}

Vertex Shader的代码相对较简单,有效的输入只有geo_Pos。EngineUBO是OpenGL的UBO变量,传入了模型、视图、投影矩阵。main方法中,计算了三角形的世界坐标系坐标、纹理坐标、输出gl_Position变量。

Fragment Shader的代码如下:

cpp 复制代码
#version 430 core

out vec4 FRAGMENT_COLOR;

layout (std140) uniform EngineUBO
{
    mat4    ubo_Model;
    mat4    ubo_View;
    mat4    ubo_Projection;
    vec3    ubo_ViewPos;
    float   ubo_Time;
};

in VS_OUT
{
    vec3 FragPos;
    vec2 TexCoords;
} fs_in;

uniform vec3 u_Color;

float MAG(float p_lp)
{
  const float lineWidth = 1.0f;

  const vec2 coord       = fs_in.TexCoords / p_lp;
  const vec2 grid        = abs(fract(coord - 0.5) - 0.5) / fwidth(coord);
  const float line       = min(grid.x, grid.y);
  const float lineResult = lineWidth - min(line, lineWidth);

  return lineResult;
}

float Grid(float height, float a, float b, float c)
{
  const float cl   = MAG(a);
  const float ml   = MAG(b);
  const float fl   = MAG(c);

  const float cmit =  10.0f;
  const float cmet =  40.0f;
  const float mfit =  80.0f;
  const float mfet =  160.0f;

  const float df   = clamp((height - cmit) / (cmet - cmit), 0.0f, 1.0f);
  const float dff  = clamp((height - mfit) / (mfet - mfit), 0.0f, 1.0f);

  const float inl  = mix(cl, ml, df);
  const float fnl  = mix(inl, fl, dff);

  return fnl;
}

void main()
{
  const float height = distance(ubo_ViewPos.y, fs_in.FragPos.y);

  const float gridA = Grid(height, 1.0f, 4.0f, 8.0f);
  const float gridB = Grid(height, 4.0f, 16.0f, 32.0f);

  const float grid  = gridA * 0.5f + gridB;

  // const vec2  viewdirW    = ubo_ViewPos.xz - fs_in.FragPos.xz;
  // const float viewdist    = length(viewdirW);
  
  FRAGMENT_COLOR = vec4(u_Color, grid);
}

Fragment shader的代码没有看太明白,需要的时候再分析吧。

三、绘制坐标轴线Shader

相比之下,绘制坐标轴线的Shader就简单太多了。线的顶点使用两个uniform变量传入线的两个顶点,根据gl_VertexID判断使用哪个顶点。FS直接给出颜色。

cpp 复制代码
############ Vertex Shader ###########

#version 430 core

uniform vec3 start;
uniform vec3 end;
uniform mat4 viewProjection;

void main()
{
	vec3 position = gl_VertexID == 0 ? start : end;
    gl_Position = viewProjection * vec4(position, 1.0);
}


########  Fragment Shader #############
#version 430 core

uniform vec3 color;

out vec4 FRAGMENT_COLOR;

void main()
{
	FRAGMENT_COLOR = vec4(color, 1.0);
}

对应CPU端的代码:

cpp 复制代码
void OvRendering::Core::ShapeDrawer::DrawLine(const OvMaths::FVector3& p_start, const OvMaths::FVector3& p_end, const OvMaths::FVector3& p_color, float p_lineWidth)
{
    // 绑定line Shader
	m_lineShader->Bind();

	m_lineShader->SetUniformVec3("start", p_start); // 线的起点
	m_lineShader->SetUniformVec3("end", p_end);     // 线的终点
	m_lineShader->SetUniformVec3("color", p_color); // 线的颜色

    // 绘制线
	m_renderer.SetRasterizationMode(OvRendering::Settings::ERasterizationMode::LINE);
	m_renderer.SetRasterizationLinesWidth(p_lineWidth);
    // 掉Draw call
	m_renderer.Draw(*m_lineMesh, Settings::EPrimitiveMode::LINES);
	m_renderer.SetRasterizationLinesWidth(1.0f);
	m_renderer.SetRasterizationMode(OvRendering::Settings::ERasterizationMode::FILL);

	m_lineShader->Unbind();
}

这里有个m_lineMesh对象,其包含两个随意的顶点即可,只是为了启动两次顶点着色器,真实的顶点坐标是靠uniform传入的。Overload将其全部初始化为0:

cpp 复制代码
std::vector<Geometry::Vertex> vertices;
	vertices.push_back
	({
		0, 0, 0,// 坐标
		0, 0,   // 纹理
		0, 0, 0,// 法线
		0, 0, 0,
		0, 0, 0
	});
	vertices.push_back
	({
		0, 0, 0,
		0, 0,
		0, 0, 0,
		0, 0, 0,
		0, 0, 0
	});

	m_lineMesh = new Resources::Mesh(vertices, { 0, 1 }, 0);
相关推荐
晓131318 分钟前
【Cocos Creator 3.x】篇——第二章 入门
前端·javascript·游戏引擎
玖玥拾2 小时前
Cocos学习笔记:粒子系统与对象层批量处理
游戏引擎·cocos2d
不知名的老吴6 小时前
Unity3D 2022安装教程及全流程下载步骤指南
unity·游戏引擎
程序员也有头发7 小时前
如何使用AI工具开发Unity
unity·游戏引擎·ai编程
caimouse7 小时前
Godot 引擎官方常见问题(FAQ)整理
游戏引擎·godot
一锅炖出任易仙8 小时前
创梦汤锅学习日记day29
学习·ai·ue5·游戏引擎
晓131310 小时前
【Cocos Creator 3.x】篇——第三章 脚本编程
前端·javascript·游戏引擎
可别39011 小时前
cesium实现网格化
游戏引擎·cocos2d
游乐码1 天前
Unity基础(十二)资源异步加载
unity·游戏引擎
weixin_441940011 天前
vuforia ar unity实验教程
unity·游戏引擎·ar