HEngine游戏引擎-31-60
三十一、设计着色器库
为什么要Shader库
为了重复使用shader,把shader存储在cpu中,需要的时候可以直接使用,不用再创建。
代码修改
Shader
1
2
3
4
5
6
7
8
9
10
11
12
13class ShaderLibrary {
public:
void Add(const std::string& name, const Ref<Shader>& shader);
void Add(const Ref<Shader>& shader);
Ref<Shader> Load(const std::string& filepath);
Ref<Shader> Load(const std::string& name, const std::string& filepath);
Ref<Shader> Get(const std::string &name);
bool Exists(const std::string& name) const;
private:
std::unordered_map<std::string, Ref<Shader>> m_Shaders;
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32void ShaderLibrary::Add(const std::string& name, const Ref<Shader>& shader)
{
HE_CORE_ASSERT(!Exists(name), "Shader already exists!");
m_Shaders[name] = shader;
}
void ShaderLibrary::Add(const Ref<Shader>& shader)
{
auto& name = shader->GetName();
Add(name, shader);
}
HEngine::Ref<HEngine::Shader> ShaderLibrary::Load(const std::string& filepath)
{
auto shader = Shader::Create(filepath);
Add(shader);
return shader;
}
HEngine::Ref<HEngine::Shader> ShaderLibrary::Load(const std::string& name, const std::string& filepath)
{
auto shader = Shader::Create(filepath);
Add(name,shader);
return shader;
}
HEngine::Ref<HEngine::Shader> ShaderLibrary::Get(const std::string& name)
{
HE_CORE_ASSERT(Exists(name), "Shader not found!");
return m_Shaders[name];
}
bool ShaderLibrary::Exists(const std::string& name) const
{
return m_Shaders.find(name) != m_Shaders.end();
}Sandboxapp.cpp
1
2
3
4
5
6
7
8
9
10m_Shader = HEngine::Shader::Create("VertexPosColor", vertexSrc, fragmentSrc);
m_FlatColorShader = HEngine::Shader::Create("FlatColor",flatColorShaderVertexSrc, flatColorShaderFragmentSrc);
auto textureShader = m_ShaderLibrary.Load("assets/shaders/Texture.glsl");
std::dynamic_pointer_cast<HEngine::OpenGLShader>(textureShader)->Bind();
std::dynamic_pointer_cast<HEngine::OpenGLShader>(textureShader)->UploadUniformInt("u_Texture", 0);
auto textureShader = m_ShaderLibrary.Get("Texture");
三十二、添加相机控制器
此节目的
在SandboxApp里面有camera移动旋转的方法,希望能将Camera抽象成一个类
关键代码
宽高比
在1280720下的界面,宽1280>高720,宽明显比高 像素占位*多。
需传入1280/720=1.7左右,将宽放大,从而左右视角变大,达到正常比例。
1
ExampleLayer() : Layer("Example"), m_CameraController(1280.0f / 720.0f, true)
m_ZoomLevel视野影响物体大小
1
m_Camera(-m_AspectRatio * m_ZoomLevel, m_AspectRatio* m_ZoomLevel,-m_ZoomLevel, m_ZoomLevel)
- 视野放大,物体缩小
- 视野缩小,物体放大
m_ZoomLevel视野影响摄像机移动速度
1
2// 视野放大,摄像机移动速度变快,视野缩小,摄像机移动速度变慢
m_CameraTranslationSpeed = m_ZoomLevel;- 视野放大,物体缩小,摄像机移动速度变快
- 视野缩小,物体放大,摄像机移动速度变慢
代码修改
新建OrthographicCameraController类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class OrthographicCameraController
{
public:
OrthographicCameraController(float aspectRatio, bool rotation = false);
void OnUpdate(Timestep ts);
void OnEvent(Event& e);
OrthographiCamera& GetCamera(){ return m_Camera; }
const OrthographiCamera& GetCamera() const { return m_Camera; }
private:
bool OnMouseScrolled(MouseScrolledEvent& e);
bool OnWindowResized(WindowResizeEvent& e);
private:
float m_AspectRatio;
float m_ZoomLevel = 1.0f;
OrthographiCamera m_Camera;
bool m_Rotation;
glm::vec3 m_CameraPosition = { 0.0f, 0.0f, 0.0f };
float m_CameraRotation = 0.0f;
float m_CameraTranslationSpeed = 10.0f, m_CameraRotationSpeed = 180.0f;
};OrthographicCameraController.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56namespace HEngine
{
OrthographicCameraController::OrthographicCameraController(float aspectRatio, bool rotation)
: m_AspectRatio(aspectRatio), m_Camera(-m_AspectRatio * m_ZoomLevel, m_AspectRatio * m_ZoomLevel, -m_ZoomLevel, m_ZoomLevel), m_Rotation(rotation)
{
}
void OrthographicCameraController::OnUpdate(Timestep ts)
{
if (Input::IsKeyPressed(HE_KEY_A))
m_CameraPosition.x -= m_CameraTranslationSpeed * ts;
else if (Input::IsKeyPressed(HE_KEY_D))
m_CameraPosition.x += m_CameraTranslationSpeed * ts;
if (Input::IsKeyPressed(HE_KEY_W))
m_CameraPosition.y += m_CameraTranslationSpeed * ts;
else if (Input::IsKeyPressed(HE_KEY_S))
m_CameraPosition.y -= m_CameraTranslationSpeed * ts;
if (m_Rotation)
{
if (Input::IsKeyPressed(HE_KEY_Q))
m_CameraRotation += m_CameraRotationSpeed * ts;
else if (Input::IsKeyPressed(HE_KEY_E))
m_CameraRotation -= m_CameraRotationSpeed * ts;
m_Camera.SetRotation(m_CameraRotation);
}
m_Camera.SetPosition(m_CameraPosition);
m_CameraTranslationSpeed = m_ZoomLevel;
}
void OrthographicCameraController::OnEvent(Event& e)
{
EventDispatcher dispatcher(e);
dispatcher.Dispatch<MouseScrolledEvent>(HE_BIND_EVENT_FN(OrthographicCameraController::OnMouseScrolled));
dispatcher.Dispatch<WindowResizeEvent>(HE_BIND_EVENT_FN(OrthographicCameraController::OnWindowResized));
}
bool OrthographicCameraController::OnMouseScrolled(MouseScrolledEvent& e)
{
m_ZoomLevel -= e.GetYOffset() * 0.25f;
m_ZoomLevel = std::max(m_ZoomLevel, 0.25f);
m_Camera.SetProjection(-m_AspectRatio * m_ZoomLevel, m_AspectRatio * m_ZoomLevel, -m_ZoomLevel, m_ZoomLevel);
return false;
}
bool OrthographicCameraController::OnWindowResized(WindowResizeEvent& e)
{
m_AspectRatio = (float)e.GetWidth() / (float)e.GetHeight();
m_Camera.SetProjection(-m_AspectRatio * m_ZoomLevel, m_AspectRatio * m_ZoomLevel, -m_ZoomLevel, m_ZoomLevel);
return false;
}
}SandboxApp.cpp
1 | ExampleLayer() |
三十三、窗口大小调整功能
三十二节虽然设置了CameraController的窗口resize事件,但是OpenGL并没有改变渲染的位置,依旧是原先的大小和位置,所以需要调整OpenGL的渲染视口。
后期我们需要为引擎实现不依靠停留在窗口也可以渲染的区域,需要用到OpenGL的frame buffer
此节目的
为了实现调整窗口大小后
OpenGL绘图的区域也会相应调整
窗口调为0*0,OpenGL不应该渲染图形了
摄像机也会根据窗口大小的变换,依旧保持正确的宽高比
关键代码
Application.cpp
使用事件调度器捕捉响应Window调整大小事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21dispatcher.Dispatch<WindowResizeEvent>(BIND_EVENT_FN(OnWindowResize));// 捕捉响应Window调整大小事件
bool Application::OnWindowResize(WindowResizeEvent& e)
{
if (e.GetWidth() == 0 || e.GetHeight() == 0)
{
m_Minimized = true;
return false;
}
m_Minimized = false;
Renderer::OnWindowResize(e.GetWidth(), e.GetHeight());
return false;
}
if (!m_Minimized) {
for (Layer* layer : m_LayerStack) {
layer->OnUpdate(timestep);
}
}OpenGL改变视口:绘图的区域改变
1
2
3
4void OpenGLRendererAPI::SetViewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height)
{
glViewport(x, y, width, height);
}窗口大小调整,摄像机需要依旧保持正确的宽高比
1
2
3
4
5
6
7bool HEngine::OrthographicCameraController::OnWindowResized(WindowResizeEvent& e)
{
// 重新计算宽高比
m_AspectRatio = (float)e.GetWidth() / (float)e.GetHeight();
m_Camera.SetProjection(-m_AspectRatio * m_ZoomLevel, m_AspectRatio * m_ZoomLevel, -m_ZoomLevel, m_ZoomLevel);
return false;
}
三十四、创建SandBox2D
此节所做
将Sandbox项目中的ExampleLayer渲染代码移到在Sandbox项目中新创建的SandBox2D(Layer)类中。
在SandBox2D类中调用HEngine项目中的OpenGLRenderer类。
代码修改
Sandbox2D.h
1 | class Sandbox2D : public HEngine::Layer |
Sandbox2D.cpp
1 | Sandbox2D::Sandbox2D() |
三十五、创建Renderer2D
- 此节所作
- 在Sandbox2D类的基础上再做一次封装,将去除关于着色器、顶点缓冲这些关于代码,只需使用简单的Api调用就能绘制单个quad图形。
- 新建一个Renderer2D类来渲染单个2Dquad图形,而不是用Rednerer类来渲染一个大场景。
2D渲染类的函数是静态的解释
- OpenGL绘图是一个设置状态顺序的过程
- 在2D渲染类中只是简单的调用设置OpenGL状态,并不需要实例化
- 不需要让一个2D渲染类开始场景,另一个2D渲染类绘制
- 综上,只需要一个就行,所以静态即可
代码改变
Renderer2D
1
2
3
4
5
6
7
8
9
10
11
12
13
14namespace HEngine
{
class Renderer2D
{
public:
static void Init();
static void Shutdown();
static void BeginScene(const OrthographiCamera& camera);
static void EndScene();
//Primitives
static void DrawQuad(const glm::vec2& position, const glm::vec2 size, const glm::vec4& color);
static void DrawQuad(const glm::vec3& position, const glm::vec2 size, const glm::vec4& color);
};
}Renderer2D.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26struct Renderer2DStorage
{
Ref<VertexArray> QuadVertexArray;
Ref<Shader> FlatColorShader;
};
static Renderer2DStorage* s_Data;
void Renderer2D::BeginScene(const OrthographiCamera& camera)
{
std::dynamic_pointer_cast<OpenGLShader>(s_Data->FlatColorShader)->Bind();
std::dynamic_pointer_cast<OpenGLShader>(s_Data->FlatColorShader)->UploadUniformMat4("u_ViewProjection", camera.GetViewProjectionMatrix());
std::dynamic_pointer_cast<OpenGLShader>(s_Data->FlatColorShader)->UploadUniformMat4("u_Transform", glm::mat4(1.0f));
}
void Renderer2D::DrawQuad(const glm::vec2& position, const glm::vec2 size, const glm::vec4& color)
{
DrawQuad({ position.x,position.y,0.0f }, size, color);
}
void Renderer2D::DrawQuad(const glm::vec3& position, const glm::vec2 size, const glm::vec4& color)
{
std::dynamic_pointer_cast<OpenGLShader>(s_Data->FlatColorShader)->Bind();
std::dynamic_pointer_cast<OpenGLShader>(s_Data->FlatColorShader)->UploadUniformFloat4("u_Color", color);
s_Data->QuadVertexArray->Bind();
RenderCommand::DrawIndexed(s_Data->QuadVertexArray);
}
Sandbox2D.cpp
1 | void Sandbox2D::OnUpdate(HEngine::Timestep ts) |
三十六、实现单个着色器
目前的Renderer2D有两个Shader,FlatColorShader和TextureShader,其实可以把两个Shader合为一个Shader,毕竟Shader的编译还是要消耗不少性能的。
这么做的思路是,使用两个uniform,一个是代表color的float4的uniform,一个是代表texture的sampler2D的uniform,片元着色器如下所示:
1 |
|
核心思路是,当我想要把Shader用作TextureShader时,那么传入我要渲染的Texture,u_Color传入(1,1,1,1)即可;当我想要把Shader用作FlatShader时,u_Color里传入我要显示的FlatColor,同时传入一个特殊的WhiteTexture,这个Texture的Sample的返回值永远是(1,1,1,1)。
Renderer2D.cpp
1 | struct Renderer2DStorage |
Sandbox2D.cpp
1 | void Sandbox2D::OnUpdate(HEngine::Timestep ts) |
三十七、添加程序性能检测功能
对于C++程序的性能优劣,程序所用时长是一个很重要的指标,为了方便预览和分析程序里想要分析的代码片段每帧所花的时间,这里选了一个很简单的方法,就是在Runtime把Profile信息写到一个JSON文件里,然后利用Chrome提供的Chrome Tracing工具(只要在谷歌浏览器里输入chrome://tracing即可打开它),来帮忙分析这个JSON文件。
实现写入JSON文件的类叫instrumentor
,这个单词在英语里其实并不存在,它源自于单词instrumentation
,本意是一套仪器、仪表,在CS里的意思是对程序性能、错误等方面的监测
所以instrumentor
可以翻译为程序性能检测者,代码如下:
1 | namespace HEngine { |
三十八、改进Renderer2D
- 此节目的,对Renderer2D API进行改进,重载DrawQuad方法进行添加旋转的功能。
改进
glsl纹理采样坐标从0-1到0-10应由CPP绘图API控制
1
color = texture(u_Texture, v_TexCoord * 10.0) * u_Color;
改为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
layout(location = 0) out vec4 color;
in vec2 v_TexCoord;
uniform vec4 u_Color;
uniform float u_TilingFactor;// 由这个控制
uniform sampler2D u_Texture;
void main() {
color = texture(u_Texture, v_TexCoord * u_TilingFactor) * u_Color;
}
代码
Renderer2D.cpp新增和扩展API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void Renderer2D::DrawRotatedQuad(const glm::vec2& position, const glm::vec2 size, float rotation, const glm::vec4& color)
{
DrawRotatedQuad({ position.x,position.y,0.0f }, size, rotation, color);
}
void Renderer2D::DrawRotatedQuad(const glm::vec3& position, const glm::vec2 size, float rotation, const glm::vec4& color)
{
HE_PROFILE_FUNCTION();
s_Data->TextureShader->SetFloat4("u_Color", color);
s_Data->TextureShader->SetFloat("u_TilingFactor", 1.0f);
s_Data->WhiteTexture->Bind();
glm::mat4 transform = glm::translate(glm::mat4(1.0f), position)
* glm::rotate(glm::mat4(1.0f), rotation, { 0.0f, 0.0f, 1.0f })
* glm::scale(glm::mat4(1.0f), { size.x, size.y, 1.0f });
s_Data->TextureShader->SetMat4("u_Transform", transform);
s_Data->QuadVertexArray->Bind();
RenderCommand::DrawIndexed(s_Data->QuadVertexArray);
}
...Sandbox2D.cpp
1
2
3
4
5void Sandbox2D::OnUpdate(HEngine::Timestep ts)
{
HEngine::Renderer2D::DrawRotatedQuad({ -1.0f, 0.0f }, { 0.8f, 0.8f }, 45.0f, { 0.8f, 0.2f, 0.3f, 1.0f });
HEngine::Renderer2D::DrawQuad({ 0.0f, 0.0f, -0.1f }, { 10.0f, 10.0f }, m_CheckerboardTexture, 10.0f, glm::vec4(1.0f, 0.9f, 0.9f, 1.0f));
}
三十九、实现批处理2D渲染
前言
目的
实现OpenGL的批处理渲染,减少OpenGL绘制命令的调用,用一次OpenGL绘制命令,绘制多个图形
大致思路
CPU和GPU都开辟同样大小的一大块内存(为了存储顶点信息)
索引在程序运行时生成对应规则后绑定到索引缓冲中
动态生成顶点信息(现在改成Drawquad只是确定图形顶点的位置)
然后在Endscene,将CPU的动态生成的顶点数据上传给GPU,然后再绘制出来。
关键代码流程
CPU和GPU都开辟同样大小的一大块内存(为了存储顶点信息)
1
2
3
4
5// 在CPU开辟存储s_Data.MaxVertices个的QuadVertex的内存
s_Data.QuadVertexBufferBase = new QuadVertex[s_Data.MaxVertices];
// 创建顶点缓冲区,先在GPU开辟一块s_Data.MaxVertices * sizeof(QuadVertex)大小的内存
s_Data.QuadVertexBuffer = VertexBuffer::Create(s_Data.MaxVertices * sizeof(QuadVertex));索引在程序运行时生成对应规则后绑定到索引缓冲中
(规则就是2个三角形组成的Quad,本来2个三角形共6个顶点,用索引后可以重复利用顶点,从而减少到4个顶点组成一个四方形)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20uint32_t* quadIndices = new uint32_t[s_Data.MaxIndices];
// 一个quad用6个索引,012 230
uint32_t offset = 0;
for (uint32_t i = 0; i < s_Data.MaxIndices; i += 6) {
quadIndices[i + 0] = offset + 0;
quadIndices[i + 1] = offset + 1;
quadIndices[i + 2] = offset + 2;
quadIndices[i + 3] = offset + 2;
quadIndices[i + 4] = offset + 3;
quadIndices[i + 5] = offset + 0;
offset += 4;
}
Ref<IndexBuffer> flatIB = IndexBuffer::Create(quadIndices, s_Data.MaxIndices);
s_Data.QuadVertexArray->SetIndexBuffer(flatIB);
delete[] quadIndices;动态生成顶点信息,主要是位置、纹理坐标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25void HEngine::Renderer2D::DrawQuad(const glm::vec3& position, const glm::vec2& size, const glm::vec4& color)
{
// quad的左下角为起点
s_Data.QuadVertexBufferPtr->Position = position;
s_Data.QuadVertexBufferPtr->Color = color;
s_Data.QuadVertexBufferPtr->TexCoord = { 0.0f, 0.0f };
s_Data.QuadVertexBufferPtr++;
s_Data.QuadVertexBufferPtr->Position = { position.x + size.x, position.y, 0.0f };
s_Data.QuadVertexBufferPtr->Color = color;
s_Data.QuadVertexBufferPtr->TexCoord = { 1.0f, 0.0f };
s_Data.QuadVertexBufferPtr++;
s_Data.QuadVertexBufferPtr->Position = { position.x + size.x, position.y + size.y, 0.0f };
s_Data.QuadVertexBufferPtr->Color = color;
s_Data.QuadVertexBufferPtr->TexCoord = { 1.0f, 1.0f };
s_Data.QuadVertexBufferPtr++;
s_Data.QuadVertexBufferPtr->Position = { position.x, position.y +size.y , 0.0f };
s_Data.QuadVertexBufferPtr->Color = color;
s_Data.QuadVertexBufferPtr->TexCoord = { 0.0f, 1.0f };
s_Data.QuadVertexBufferPtr++;
s_Data.QuadIndexCount += 6;// 每一个quad用6个索引
}然后在Endscene,将CPU的动态生成的顶点数据(主要是位置信息)上传给GPU,然后再绘制出来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void HEngine::Renderer2D::EndScene()
{
// 计算当前绘制需要多少个顶点数据
uint32_t dataSize = (uint8_t*)s_Data.QuadVertexBufferPtr - (uint8_t*)s_Data.QuadVertexBufferBase;
// 截取部分CPU的顶点数据上传OpenGL
s_Data.QuadVertexBuffer->SetData(s_Data.QuadVertexBufferBase, dataSize);
Flush();
}
void Renderer2D::Flush()
{
RenderCommand::DrawIndexed(s_Data.QuadVertexArray, s_Data.QuadIndexCount);
}
......
void OpenGLVertexBuffer::SetData(const void* data, uint32_t size)
{
glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);
// 截取部分CPU的顶点数据上传OpenGL
glBufferSubData(GL_ARRAY_BUFFER, 0, size, data);
}Glsl的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec4 a_Color;
layout(location = 2) in vec2 a_TexCoord;
uniform mat4 u_ViewProjection;
// uniform mat4 u_Transform;
out vec4 v_Color;
out vec2 v_TexCoord;
void main() {
v_Color = a_Color;
v_TexCoord = a_TexCoord;
// 由规则动态生成的顶点位置(基于本地空间)没有涉及transform变换顶点位置
// gl_Position = u_ViewProjection * u_Transform * vec4(a_Position, 1.0);
gl_Position = u_ViewProjection * vec4(a_Position, 1.0);
}
四十、 Batch Textures
前言
回顾纹理采样个人理解。
- 加载纹理资源,设置这个纹理资源开辟的缓冲区m_RendererID绑定在纹理槽0号上
- glsl上采样的纹理,在纹理槽0号上采样,纹理槽0号指向了第1步的纹理资源,这样就完成对这个加载的纹理采样。
OpenGL的纹理槽
OpenGL限制了一次drawcall能使用多少个纹理槽,HEngine设置为32个
若想超过32有40个纹理需要使用,可以:
- 分两次,第一次drawcall32个,然后清空重新加载纹理再drawcall
- 用纹理集,texture altas
大致流程
先提前为此次的shader上传一个默认的大小为32的采样数组
u_Textures[i] = i,u_Textures[1] = 1表示片段着色器采样纹理槽1号上的纹理
加载一个纹理得到纹理对象,用数组保存这个纹理对象
在绘制带有纹理的quad图形时,判断数组中是否有这个纹理对象
设置当前顶点采样的纹理单元是 i,后续会将这个纹理槽号 i 从顶点阶段传入到fragment片段着色器阶段
在Drawcall前,TextureSlots数组上存储已经加载的纹理,按照顺序依次绑定到对应的纹理槽上
Drawcall时,在片段着色器上,读取采样对应纹理槽号i上的纹理
代码
先记住数据结构
std::array TextureSlots;
是一个32大小的数组,数组的元素是纹理对象指针,用来存储加载好了的纹理对象的
int32_t samplers[s_Data.MaxTextureSlots];
是一个32大小的数组,数组的元素是int值,用来上传给glsl的
先提前为此次的shader上传一个默认的大小为32的采样数组,u_Textures[i] = j, 其中i = j,u_Textures[1] = 1表示采样纹理槽1号上的纹理
1
2
3
4
5
6
7
8
9
10// 纹理的shader
s_Data.TextureShader = Shader::Create("assets/shaders/Texture.glsl");
int32_t samplers[s_Data.MaxTextureSlots];
for (uint32_t i = 0; i < s_Data.MaxTextureSlots; i++) {
samplers[i] = i;
}
s_Data.TextureShader->Bind();
// 为TextureShader上传一个默认的大小为32的采样数组
s_Data.TextureShader->SetIntArray("u_Textures", samplers, s_Data.MaxTextureSlots);加载一个纹理得到纹理对象,用数组保存这个纹理对象
1
2
3
4
5
6
7
8// 白色纹理
// 创建一个白色Texture
s_Data.WhiteTexture = Texture2D::Create(1, 1);
uint32_t whiteTextureData = 0xffffffff;
s_Data.WhiteTexture->SetData(&whiteTextureData, sizeof(uint32_t));
// 0号纹理槽对应白色纹理缓冲区
s_Data.TextureSlots[0] = s_Data.WhiteTexture;在当前绘制quad图形时,判断TextureSlots数组是否有这个纹理,有就取出 i 下标,没有就加在数组已有纹理的末尾,并且记录下标 i
1
2
3
4
5
6
7
8
9
10
11
12
13
14float textureIndex = 0.0f;
for (uint32_t i = 1; i < s_Data.TextureSlotIndex; i++)
{
// 当前纹理,如果已经存储在纹理槽,就直接读取
if (*s_Data.TextureSlots[i].get() == *texture.get()) {
textureIndex = (float)i;
break;
}
}
if (textureIndex == 0.0f) {
textureIndex = (float)s_Data.TextureSlotIndex;
s_Data.TextureSlots[s_Data.TextureSlotIndex] = texture;
s_Data.TextureSlotIndex++;
}在顶点数据数组设置当前顶点采样的纹理单元是 i,后续在第6步会将这个纹理槽号 i 传入到fragment阶段
1
2
3
4
5
6
7// quad的左下角为起点
s_Data.QuadVertexBufferPtr->Position = position;
s_Data.QuadVertexBufferPtr->Color = color;
s_Data.QuadVertexBufferPtr->TexCoord = { 0.0f, 0.0f };
s_Data.QuadVertexBufferPtr->TexIndex = textureIndex; // 采样的纹理槽号
s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
s_Data.QuadVertexBufferPtr++;glsl相关
1
2
3
4
5
6
7
8
9in float v_TexIndex;
in float v_TilingFactor;
uniform sampler2D u_Textures[32];
void main()
{
color = texture(u_Textures[int(v_TexIndex)], v_TexCoord * v_TilingFactor) * v_Color;
}在Drawcall前,TextureSlots数组上存储已经加载的纹理,按照顺序依次绑定到对应的纹理槽上
这样就与第3、4步设置当前顶点采样的纹理槽号i是TextureSlots数组上的i号纹理
1
2
3
4
5
6
7
8void Renderer2D::Flush()
{
// 对应i的texture绑定到i号纹理槽
for (uint32_t i = 0; i < s_Data.TextureSlotIndex; i++) {
s_Data.TextureSlots[i]->Bind(i);
}
RenderCommand::DrawIndexed(s_Data.QuadVertexArray, s_Data.QuadIndexCount);
}
四十一、优化批处理功能
前言
前两节写的批处理留下来的问题
- 批处理没有处理Quad旋转的函数
- 设计的顶点位置是基于自身空间的,没有经过transform变换位置,所以无旋转效果
此节所做
处理Quad旋转函数
将顶点位置在Cpu上计算,通过transform矩阵(平移、缩放、旋转)变换到世界空间
与060之前不同,之前将顶点从局部空间转换到世界空间是在GPU(GLSL代码)上运行的。
流程:
给所有quad以原点为初始位置(局部空间)
再接受旋转角度,用初始位置transform矩阵 = 平移旋转缩放,再乘以顶点转换到世界空间
(提下,写代码顺序是:平移旋转缩放,但解读顺序是:从右往左读,先进行缩放,再进行旋转最后平移)
将最终世界空间的位置上传到GLSL的顶点着色器阶段
关键代码
Renderer2D.cpp
1 | void Renderer2D::DrawrRotatedQuad(const glm::vec3& position, const glm::vec2& size, float rotation, const Ref<Texture2D>& texture, float tilingFactor, const glm::vec4& tintColor) |
Sandbox2D.cpp
1 | void Sandbox2D::OnUpdate(HEngine::Timestep ts) |
四十二、重置绘画缓存
前言
之前批处理设计带来的问题
当绘画的图形所占的内存超过我们得预先给定的空间,应该分两次Drawcall
第二次drawcall时候需要重置内存数据,以便能开始下一轮批处理。
此节代码思路
需要知道当前渲染信息
1
2
3
4
5
6
7
8
9Renderer2D.h
// 当前渲染的信息
struct Statistics {
uint32_t DrawCalls = 0;
uint32_t QuadCount = 0;
uint32_t GetTotalVertexCount() { return QuadCount * 4; }
uint32_t GetTotalIndexCount() { return QuadCount * 6; }
};设置最大绘制数量
1
2
3
4
5
6
7Renderer2D.cpp
struct Renderer2DData {
static const uint32_t MaxQuads = 20000;// 一次绘制多少个Quad
.....
Renderer2D::Statistics Stats;
};当绘画的图形所占的内存超过我们得预先给定的空间,需要有判定(提交渲染和重置)
1
2
3
4
5if (s_Data.QuadIndexCount >= Renderer2DData::MaxIndices) {// 判断需要提交渲染和重置
FlushAndReset();
}
......
s_Data.Stats.QuadCount++;当一次渲染超过这个数量,分两次渲染,第二次渲染时候需要重置内存数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// 内存不够为了分批渲染要做的drawcall绘制和重置
void Renderer2D::FlushAndReset()
{
EndScene();
//重置
s_Data.QuadIndexCount = 0;
s_Data.QuadVertexBufferPtr = s_Data.QuadVertexBufferBase;
s_Data.TextureSlotIndex = 1;
}
void Renderer2D::EndScene{
// 计算上传大小
uint32_t dataSize = (uint8_t*)s_Data.QuadVertexBufferPtr - (uint8_t*)s_Data.QuadVertexBufferBase;
s_Data.QuadVertexBuffer->SetData(s_Data.QuadVertexBufferBase, dataSize);
Flush();
}
void Renderer2D::Flush(){
// Bind textures
for (uint32_t i = 0; i < s_Data.TextureSlotIndex; i++)
s_Data.TextureSlots[i]->Bind(i);
RenderCommand::DrawIndexed(s_Data.QuadVertexArray, s_Data.QuadIndexCount);
s_Data.Stats.DrawCalls++;
}
.........................
void OpenGLVertexBuffer::SetData(const void* data, uint32_t size){
glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);
glBufferSubData(GL_ARRAY_BUFFER, 0, size, data);
}调用绘制Render2D API代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void Sandbox2D::OnUpdate(HEngine::Timestep ts)
{
HE_PROFILE_FUNCTION();
m_CameraController.OnUpdate(ts);
// 渲染信息初始化
HEngine::Renderer2D::ResetStats();
......
HEngine::Renderer2D::BeginScene(m_CameraController.GetCamera());
HEngine::Renderer2D::DrawrRotatedQuad({ 1.0f, 0.5f }, { 0.8f, 0.8f },30.0f, m_FlatColor);
HEngine::Renderer2D::DrawQuad({ -1.0f, 0.0f }, { 0.8f, 0.8f }, m_FlatColor);
HEngine::Renderer2D::DrawQuad({ 0.5f, -0.5f }, { 0.5f, 0.8f }, {0.2f, 0.8f, 0.9f, 1.0f});
HEngine::Renderer2D::DrawQuad({ 0.0f, 0.0f, -0.1f }, { 20.0f, 20.0f }, m_SquareTexture, 10.0f);
HEngine::Renderer2D::DrawrRotatedQuad({ -0.5f, -1.5f, 0.0f }, { 1.0f, 1.0f }, rotation, m_SquareTexture, 20.0f);
HEngine::Renderer2D::EndScene();
......
四十三、 使用ImGui渲染纹理
前言
- 接下来想做的
- 使HEngine成为一个独立的工具
- 可以打开成窗口应用程序
- 可以操作程序添加场景
- 在场景上放入精灵、实体,再写脚本语言给实体添加一些行为
- 以及给实体添加组件
- 然后可以导出为游戏执行文件,可以在编辑器之外运行
Dockspace
什么是dockspace
imgui提供的可以停靠在窗口上,能够实现重新布局窗口的功能。
缺点
问题所在
使用了Imgui的dockspace,OpenGL绘制的场景不见了,因为imgui接管了窗口
如何解决
让Opengl渲染到framebuffer帧缓冲中,作为texture纹理,然后Imgui再渲染这个texture。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39void Sandbox2D::OnImgGuiRender()
{
.....
{
ImGui::Begin("Settings");
auto stats = HEngine::Renderer2D::GetStats();
ImGui::Text("Renderer2D Stats:");
ImGui::Text("Draw Calls: %d", stats.DrawCalls);
ImGui::Text("Quads: %d", stats.QuadCount);
ImGui::Text("Vertices: %d", stats.GetTotalVertexCount());
ImGui::Text("Indices: %d", stats.GetTotalIndexCount());
ImGui::ColorEdit4("Square Color", glm::value_ptr(m_SquareColor));
uint32_t textureID = m_CheckerboardTexture->GetRendererID();
ImGui::Image((void*)textureID, ImVec2{ 256.0f, 256.0f });
ImGui::End();
ImGui::End();
}
else
{
ImGui::Begin("Settings");
auto stats = HEngine::Renderer2D::GetStats();
ImGui::Text("Renderer2D Stats:");
ImGui::Text("Draw Calls: %d", stats.DrawCalls);
ImGui::Text("Quads: %d", stats.QuadCount);
ImGui::Text("Vertices: %d", stats.GetTotalVertexCount());
ImGui::Text("Indices: %d", stats.GetTotalIndexCount());
ImGui::ColorEdit4("Square Color", glm::value_ptr(m_SquareColor));
uint32_t textureID = m_CheckerboardTexture->GetRendererID();
ImGui::Image((void*)textureID, ImVec2{ 256.0f, 256.0f });
ImGui::End();
}
}
四十四、实现自定义帧缓冲
前言
帧缓冲(我的理解)
- 可以将OpenGL渲染的场景放在这个帧缓冲中
- 然后可以把这个帧缓冲当做是颜色或者纹理采样区(取决于帧缓冲附加的缓冲附件类型)
- 在ImGui把这个帧缓冲当做颜色纹理渲染出来,就在ImGui界面上显示了原本应显示在屏幕上的场景
代码流程
创建帧缓冲并附加纹理、深度与模板缓冲纹理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28void OpenGLFramebuffer::Invalidate()
{
// 1.创建帧缓冲
glCreateFramebuffers(1, &m_RendererID);
glBindFramebuffer(GL_FRAMEBUFFER, m_RendererID); // 绑定这个帧缓冲
// 2.创建纹理
glCreateTextures(GL_TEXTURE_2D, 1, &m_ColorAttachment);;
glBindTexture(GL_TEXTURE_2D, m_ColorAttachment);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, m_Specification.Width, m_Specification.Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 1.1纹理附加到帧缓冲
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_ColorAttachment, 0);
// 3.创建深度模板缓冲纹理附加到帧缓冲中
glCreateTextures(GL_TEXTURE_2D, 1, &m_DepthAttachment);;
glBindTexture(GL_TEXTURE_2D, m_DepthAttachment);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, m_Specification.Width, m_Specification.Height);
// 1.2深度模板缓冲纹理附加到帧缓冲中
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, m_DepthAttachment, 0);
HE_CORE_ASSERT(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "帧缓冲未创建完成");
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}给创建的帧缓冲写入数据
- 绑定自定义的帧缓冲
- 正常渲染图形,将本由OpenGL渲染在屏幕上的图像写入到自定义的帧缓冲中
- 解绑帧缓冲
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16void Sandbox2D::OnUpdate(HEngine::Timestep ts)
{
HEngine::Renderer2D::ResetStats();
{
m_Framebuffer->Bind();
HEngine::RenderCommand::SetClearColor({ 0.1f, 0.1f, 0.1f, 1 });
HEngine::RenderCommand::Clear();
HEngine::Renderer2D::DrawrRotatedQuad({ 1.0f, 0.5f }, { 0.8f, 0.8f },30.0f, m_FlatColor);
HEngine::Renderer2D::DrawQuad({ -1.0f, 0.0f }, { 0.8f, 0.8f }, m_FlatColor);
.......
// 3.解绑帧缓冲
m_Framebuffer->Unbind();
}
......
}Imgui渲染自定义的帧缓冲
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16ImGui::Begin("Settings");
auto stats = HEngine::Renderer2D::GetStats();
ImGui::Text("Renderer2D Stats:");
ImGui::Text("Draw Calls: %d", stats.DrawCalls);
ImGui::Text("Quads: %d", stats.QuadCount);
ImGui::Text("Vertices: %d", stats.GetTotalVertexCount());
ImGui::Text("Indices: %d", stats.GetTotalIndexCount());
ImGui::ColorEdit4("Square Color", glm::value_ptr(m_SquareColor));
//在执行完Invalidate方法后,所有渲染内容都会存储到这个FBO的附件(ColorAttachment)中
//此时只要渲染这个纹理就会得出原来屏幕的画面
uint32_t textureID = m_Framebuffer->GetColorAttachmentRendererID();
ImGui::Image((void*)textureID, ImVec2{ 1280, 720 }, ImVec2{ 0, 1}, ImVec2{ 1, 0 });
ImGui::End();
BUG:ImGui上显示OpenGL渲染的图像
Bug说明
可视化界面的wasd摄像机移动,显示相反(按a却成d的效果)。
Bug分析
由于imgui的uv默认是左下角为01,右下角为11,左上角为00,右上角是10(起始点在左上角)
而我们绘制的quad的uv是左下角为00,右下角10,左上角01,右上角11(起始点在左下角)
调整
1
2
3
4
5/*
ImVec2(0, 1):设置左上角点的uv是 0 1
ImVec2(1, 0):设置右下角点的uv是 1 0
*/
ImGui::Image((void*)textureID, ImVec2( 1280, 720 ), ImVec2(0, 1), ImVec2(1, 0));
补充:创建HEngine-Editor新项目并设置为启动项目, 用来做可视化界面,构建游戏,原先Sandbox项目的操作移动到Editor里面
四十五、调整视口大小处理
前言
- 此节目标
- 实现调整ImGui视口大小后,摄像机、绘图的区域、帧缓冲纹理也相应调整
代码流程
EditorLayer.cpp
1
2
3
4
5
6
7
8
9
10
11ImVec2 viewportPanelSize = ImGui::GetContentRegionAvail();
if (m_ViewportSize != *((glm::vec2*)&viewportPanelSize)) { // 如果改变了窗口大小
// 调整帧缓冲区
m_Framebuffer->Resize((uint32_t)viewportPanelSize.x, (uint32_t)viewportPanelSize.y);
m_ViewportSize = { viewportPanelSize.x, viewportPanelSize.y };
// 调整摄像机
m_CameraController.OnResize(viewportPanelSize.x, viewportPanelSize.y);
}
uint32_t textureID = m_Framebuffer->GetColorAttachmentRendererID();
ImGui::Image((void*)textureID, ImVec2(m_ViewportSize.x, m_ViewportSize.y), ImVec2(0, 1), ImVec2(1, 0));OpenGLFramebuffer.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14void OpenGLFramebuffer::Resize(uint32_t width, uint32_t height)
{
m_Specification.Width = width;
m_Specification.Height = height;
Invalidate(); //重新生成帧缓冲
}
// 在下次帧缓冲被绑定时,OpenGL视口大小被设置为imgui视口大小
void OpenGLFramebuffer::Bind()
{
glBindFramebuffer(GL_FRAMEBUFFER, m_RendererID);
glViewport(0, 0, m_Specification.Width, m_Specification.Height);
}
四十六、事件触发判定优化
发现一个bug,如鼠标放在ImGui的Settings窗口区域滑动鼠标滚轮,渲染方块的Viewport窗口也会响应,这就有问题了,应该是我鼠标放在窗口区域内并且点击了代表我选择这个窗口了,才能响应事件
解决方法:由ImGui视口能获取焦点和鼠标停留bool值,用这两个bool值来处理
代码:
EditorLayer.cpp
1 | void EditorLayer::OnUpdate(HEngine::Timestep ts) |
ImGuiLayer.cpp
1 | if (m_BlockEvents) //在鼠标既不在渲染窗口位置也没点击选中时才能响应ImGui其他窗口 |
四十七、设计实体组件系统
关于ECS的介绍单独写了比较详细的文章,实体组件系统(ECS)
HEngine选择使用提供ECS模式的Entt库来实现我们的实体组件系统,Entt相关的内容可以直接看对应的github仓库的介绍,这里是他的Wiki
该库所有的内容,应该都放到一个叫entt.hpp
的文件里了,我看了下,这个文件非常大,一共有17600行,就把它当头文件用就行了
代码:
创建Scene类
1 |
|
Scene.cpp
1 | // 用于后面的Callback例子, 当Transform组件被创建时调用, 会加到entity上 |
ComPonents.h
1 | struct TransformComponent |
四十八、添加Entity类
前言
此节目的
- 创建出Entity实体类,作为中介连接场景与组件,能添加、删除组件
思考Entt的API优缺点来设计出Scene、Entity类,之前学习到一个思路,设计一个API的时候应该先想象他需要拥有什么功能,然后再根据列出来的功能逐个实现就好了
游戏引擎开发人员的角度,是场景拥有实体、场景给实体添加组件,所以以下调用Entt代码是合理的。
1
2
3// 场景给square实体添加组件
m_ActiveScene->Reg().emplace<TransformComponent>(square); // 先要获取注册表才能添加组件
m_ActiveScene->Reg().emplace<SpriteRendererComponent>(square, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f));但是,这代码太长了,引擎开发人员应该站在游戏开发人员角度思考
- 场景虽然拥有实体,可以获取实体给它添加组件
- 实体也应拥有组件,只需简单的考虑实体添加组件这个行为,而忽略场景这个东西
- 这有利于我们简化代码,目标是简化成Unity那样给gameobject添加组件的函数
1
2
3
4
5
6// 原先:场景给square实体添加组件
m_ActiveScene->Reg().emplace<TransformComponent>(square); // 先要获取注册表才能添加组件
m_ActiveScene->Reg().emplace<SpriteRendererComponent>(square, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f));
// 化简为:square实体添加组件,忽略场景
square.AddComponent<TransformComponent>();
square.AddComponent<SpriteRendererComponent>(glm::vec4(0.0f, 1.0f, 0.0f, 1.0f));
代码
Entity.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42namespace HEngine
{
class Entity
{
public:
Entity() = default;
Entity(entt::entity handle, Scene* scene);
Entity(const Entity& other) = default;
template<typename T, typename... Args>
T& AddComponent(Args&&... args)
{
HE_CORE_ASSERT(!HasComponent<T>(), "Entity already has component!");
return m_Scene->m_Registry.emplace<T>(m_EntityHandle, std::forward<Args>(args)...);
}
template<typename T>
T& GetComponent()
{
HE_CORE_ASSERT(HasComponent<T>(), "Entity does not have component!");
return m_Scene->m_Registry.get<T>(m_EntityHandle);
}
template<typename T>
bool HasComponent()
{
return m_Scene->m_Registry.all_of<T>(m_EntityHandle);
}
template<typename T>
void RemoveComponent()
{
HE_CORE_ASSERT(HasComponent<T>(), "Entity does not have component!");
m_Scene->m_Registry.remove<T>(m_EntityHandle);
}
operator bool() const { return (uint32_t)m_EntityHandle != 0; }
private:
entt::entity m_EntityHandle{ null };
Scene* m_Scene = nullptr;
};
}Entity.cpp
1
2
3
4
5
6
7namespace HEngine
{
Entity::Entity(entt::entity handle, Scene* scene)
: m_EntityHandle(handle), m_Scene(scene)
{
}
}Scene.h
1
2
3
4
5entt::entity CreateEntity();//删除
Entity CreateEntity(const std::string& name = std::string());
private:
friend class Entity;Scene.cpp
1
2
3
4
5
6
7
8Entity Scene::CreateEntity(const std::string& name)
{
Entity entity = { m_Registry.create(), this };
entity.AddComponent<TransformComponent>();
auto& tag = entity.AddComponent<TagComponent>();
tag.Tag = name.empty() ? "Entity" : name;
return entity;
}EditorLayer.cpp使用
1
2
3
4
5
6
7ImGui::Separator();
auto& tag = m_SquaerEntity.GetComponent<TagComponent>().Tag;
ImGui::Text("%s", tag.c_str());
auto& squareColor = m_SquaerEntity.GetComponent<SpriteRendererComponent>().Color;
ImGui::ColorEdit4("Square Color", glm::value_ptr(squareColor));
ImGui::Separator();
四十九、添加摄像头组件
此节内容
设计一个Camera组件
这个Camera组件需拥有Camera类指针
Camera类拥有的属性与行为
- 设置正交投影projection矩阵
- 是否主相机
- 获取projection投影矩阵
Camera类与Camera组件设计
创建Camera.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14namespace HEngine
{
class Camera
{
public:
Camera(const glm::mat4& projection)
: m_Projection(projection) {}
const glm::mat4& GetProjection() const { return m_Projection; }
private:
glm::mat4 m_Projection;
};
}Components.h
1
2
3
4
5
6
7
8
9struct CameraComponent {
Camera camera;
bool primary = true;
CameraComponent() = default;
CameraComponent(const CameraComponent&) = default;
CameraComponent(const glm::mat4 & projection)
: camera(projection) {}
};
场景切换到主摄像机视角代码流程
EditorLayer层添加一个实体
1
2
3
4Entity m_CameraEntity;
Entity m_SecondCamera;
bool m_PrimaryCamera = true;EditorLayer.cpp
1
2
3
4
5
6m_CameraEntity = m_ActiveScene->CreateEntity("Camera Entity");
m_CameraEntity.AddComponent<CameraComponent>(glm::ortho(-16.0f, 16.0f, -9.0f, 9.0f, -1.0f, 1.0f));
m_SecondCamera = m_ActiveScene->CreateEntity("Clip-Space Entity");
auto& cc = m_SecondCamera.AddComponent<CameraComponent>(glm::ortho(-1.0f,1.0f,-1.0f,1.0f,-1.0f,1.0f));
cc.Primary = false;在scene onupdate方法中寻找主摄像机,以及获取主摄像机的transform用来计算视图矩阵
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31void Scene::OnUpdate(Timestep ts)
{
Camera* mainCamera = nullptr;
glm::mat4* cameraTransform = nullptr;
{
auto group = m_Registry.view<TransformComponent,CameraComponent>();
for (auto entity : group)
{
auto& [transform, camera] = group.get<TransformComponent, CameraComponent>(entity);
if (camera.Primary)
{
mainCamera = &camera.Camera;
cameraTransform = &transform.Transform;
break;
}
}
}
if(mainCamera)
{
Renderer2D::BeginScene(mainCamera->GetProjection(), *cameraTransform);
auto group = m_Registry.group < TransformComponent>(entt::get<SpriteRendererComponent>);
for (auto entity : group)
{
auto& [transform, sprite] = group.get<TransformComponent, SpriteRendererComponent>(entity);
Renderer2D::DrawQuad(transform, sprite.Color);
}
Renderer2D::EndScene();
}
}Renderer2D.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13void Renderer2D::BeginScene(const Camera& camera, const glm::mat4& transform)
{
HE_PROFILE_FUNCTION();
glm::mat4 viewProj = camera.GetProjection() * glm::inverse(transform);
s_Data.TextureShader->Bind();
s_Data.TextureShader->SetMat4("u_ViewProjection", viewProj);
s_Data.QuadIndexCount = 0;
s_Data.QuadVertexBufferPtr = s_Data.QuadVertexBufferBase;
s_Data.TextureSlotIndex = 1;
}
五十、 Scene Camera
此节目的
当ImGui视口大小发生改变时,场景中所有实体所拥有的摄像机都能正确的设置宽高比,使图像不会变形。
- 此节所作
- 新建了一个SceneCamera类继承自Camera类,作为摄像机组件的摄像机类属性
- 视口改变,场景内所有摄像机都能更新宽高比
- 此节所作
代码流程
修改了Camera.h
1
2
3
4
5
6
7
8
9
10
11namespace HEngine
{
class Camera
{
public:
Camera() = default;
virtual ~Camera() = default;
protected:
glm::mat4 m_Projection = glm::mat4(1.0f);
};
}新添加了SceneCamera
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23namespace HEngine
{
class SceneCamera : public Camera
{
public:
SceneCamera();
virtual ~SceneCamera() = default;
void SetOrthographic(float size, float nearClip, float farClip);
void SetViewportSize(uint32_t width, uint32_t height);
float GetOrthographicSize() const { return m_OrthographicSize; }
void SetOrthographicSize(float size) { m_OrthographicSize = size; RecalculateProjection(); }
private:
void RecalculateProjection();
private:
float m_OrthographicSize = 10.0f;
float m_OrthographicNear = -1.0f, m_OrthographicFar = 1.0f;
float m_AspectRatio = 0.0f;
};
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29namespace HEngine
{
SceneCamera::SceneCamera()
{
RecalculateProjection();
}
void SceneCamera::SetOrthographic(float size, float nearClip, float farClip)
{
m_OrthographicSize = size;
m_OrthographicNear = nearClip;
m_OrthographicFar = farClip;
RecalculateProjection();
}
void SceneCamera::SetViewportSize(uint32_t width, uint32_t height)
{
m_AspectRatio = (float)width / (float)height;
RecalculateProjection();
}
void SceneCamera::RecalculateProjection()
{
float orthoLeft = -m_OrthographicSize * m_AspectRatio * 0.5f;
float orhoRight = m_OrthographicSize * m_AspectRatio * 0.5f;
float orthoBottom = -m_OrthographicSize * 0.5f;
float orthoTop = m_OrthographicSize * 0.5f;
m_Projection = glm::ortho(orthoLeft, orhoRight,
orthoBottom, orthoTop, m_OrthographicNear, m_OrthographicFar);
}
}Scene.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14void Scene::OnViewportResize(uint32_t width, uint32_t height)
{
m_ViewportWidth = width;
m_ViewportHeight = height;
//Resize our non-FixedAspectRatio cameras
auto view = m_Registry.view<CameraComponent>();
for (auto entity : view)
{
auto& cameraComponent = view.get<CameraComponent>(entity);
if (!cameraComponent.FixedAspectRatio)
cameraComponent.Camera.SetViewportSize(width, height);
}
}EditorLayer.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void EditorLayer::OnUpdate(Timestep ts)
{
if (FramebufferSpecification spec = m_Framebuffer->GetSpecification();
m_ViewportSize.x > 0.0f && m_ViewportSize.y > 0.0f && //zero sized framebuffer is invalid
(spec.Width != m_ViewportSize.x || spec.Height != m_ViewportSize.y))
{
m_Framebuffer->Resize((uint32_t)m_ViewportSize.x, (uint32_t)m_ViewportSize.y);
m_CameraController.OnResize(m_ViewportSize.x, m_ViewportSize.y);
m_ActiveScene->OnViewportResize((uint32_t)m_ViewportSize.x, (uint32_t)m_ViewportSize.y);
}
}
void EditorLayer::OnImGuiRender()
{
{
auto& camera = m_SecondCamera.GetComponent<CameraComponent>().Camera;
float orhoSize = camera.GetOrthographicSize();
if (ImGui::DragFloat("Second Camera OrthO Size", &orhoSize))
camera.SetOrthographicSize(orhoSize);
}
}
五十一、提供C++脚本功能
前言
此节目标
需要给实体添加脚本组件Native Scripting,实现可以用cpp语言来编写脚本
此节所作
给摄像机实体添加脚本组件,在引擎内的Scene视口中引擎人员和游戏开发人员都可以控制摄像机移动
为什么需要添加cpp脚本组件
- 引擎开发人员需要给引擎某个实体添加脚本功能,从而实现测试时候能运行想要的脚本功能。
实现在Scene视口操作摄像机移动
EditorLayer.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28class CameraController : public ScriptableEntity
{
public:
void OnCreate()
{
}
void OnDestroy()
{
}
void OnUpdate(Timestep ts)
{
auto& transform = GetComponent<TransformComponent>().Transform;
float speed = 5.0f;
if (Input::IsKeyPressed(KeyCode::A))
transform[3][0] -= speed * ts;
if (Input::IsKeyPressed(KeyCode::D))
transform[3][0] += speed * ts;
if (Input::IsKeyPressed(KeyCode::W))
transform[3][1] += speed * ts;
if (Input::IsKeyPressed(KeyCode::S))
transform[3][1] -= speed * ts;
}
};
m_CameraEntity.AddComponent<NativeScriptComponent>().Bind<CameraController>();Component.h添加NativeScriptComponent脚本组件接收到CameraController作为参数T
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22struct NativeScriptComponent
{
ScriptableEntity* Instance = nullptr;
std::function<void()> InstantiateFunction;
std::function<void()> DestroyInstanceFunction;
std::function<void(ScriptableEntity*)> OnCreateFunction;
std::function<void(ScriptableEntity*)> OnDestroyFunction;
std::function<void(ScriptableEntity*, Timestep)> OnUpdateFunction;
template<typename T>
void Bind()
{
InstantiateFunction = [&]() { Instance = new T(); };
DestroyInstanceFunction = [&]() { delete (T*)Instance; Instance = nullptr; };
OnCreateFunction = [](ScriptableEntity* instance) { ((T*)instance)->OnCreate(); };
OnDestroyFunction = [](ScriptableEntity* instance) { ((T*)instance)->OnDestroy(); };
OnUpdateFunction = [](ScriptableEntity* instance, Timestep ts) { ((T*)instance)->OnUpdate(ts); };
}
};在Scene.cpp的OnUpdate函数中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16{
m_Registry.view<NativeScriptComponent>().each([=](auto entity, auto& nsc)
{
if (!nsc.Instance)
{
nsc.InstantiateFunction();
nsc.Instance->m_Entity = Entity{ entity, this };
if (nsc.OnCreateFunction)
nsc.OnCreateFunction(nsc.Instance);
}
if (nsc.OnUpdateFunction)
nsc.OnUpdateFunction(nsc.Instance, ts);
});
}ScriptableEntity.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15namespace HEngine
{
class ScriptableEntity
{
public:
template<typename T>
T& GetComponent()
{
return m_Entity.GetComponent<T>();
}
private:
Entity m_Entity;
friend class Scene;
};
}
五十二、优化C++脚本函数
前言
这节目的
将上一节在NativeScriptComponent写的function执行的功能替换为虚函数(OnCreate、OnUpdate、OnDestroy)和函数指针(Instantiate、Destroy)
代码
ScriptableEntity.h 函数定义为虚函数
1
2
3
4
5
6
7
8
9class ScriptableEntity {
public:
virtual ~ScriptableEntity() {}
protected:
virtual void OnCreate() {}
virtual void OnDestroy() {}
virtual void OnUpdate(Timestep ts) {}
};EditorLayer.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class CameraController : public ScriptableEntity
{
public:
virtual void OnCreate() override
{
auto& transform = GetComponent<TransformComponent>().Transform;
transform[3][0] = rand() % 10 - 5.0f;
}
virtual void OnDestory() override
{
}
virtual void OnUpdate(Timestep ts) override
{
...
}
}Components.h
1
2
3
4
5
6
7
8
9
10
11
12struct NativeScriptComponent
{
ScriptableEntity* (*InstantiateScript)();
void (*DestroyScript)(NativeScriptComponent*);
template<typename T>
void Bind()
{
InstantiateScript = []() {return static_cast<ScriptableEntity*>(new T()); };
DestroyScript = [](NativeScriptComponent* nsc) {delete nsc->Instance; nsc->Instance = nullptr; };
}
};Scene.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18void Scene::OnUpdate(Timestep ts)
{
// Update scripts
{
m_Registry.view<NativeScriptComponent>().each([=](auto entity, auto& nsc)
{
//TODO: Move to Scene::OnScenePlay
if (!nsc.Instance)
{
nsc.Instance = nsc.InstantiateScript();
nsc.Instance->m_Entity = Entity{ entity, this };
nsc.Instance->OnCreate();
}
nsc.Instance->OnUpdate(ts);
});
}
}
五十三、场景Hierarchy面板
前言
此节目的
给引擎添加hierarchy界面面板,用ECS遍历当前场景的实体,用imgui显示当前场景存活的实体
代码
EditorLayer.h
1
SceneHierarchyPanel m_SceneHierarchyPanel;
EditorLayer.cpp
1
2
3
4
5
6
7
8void EditorLayer::OnAttach()
{
m_SceneHierarchyPanel.SetContext(m_ActiveScene);// 设置上下文
}
void EditorLayer::OnImGuiRender()
{
m_SceneHierarchyPanel.OnImGuiRender();
}SceneHierarchyPanel.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18namespace HEngine
{
class SceneHierarchyPanel
{
public:
SceneHierarchyPanel() = default;
SceneHierarchyPanel(const Ref<Scene>& scene);
void SetContext(const Ref<Scene>& scene);
void OnImGuiRender();
private:
void DrawEntityNode(Entity entity);
private:
Ref<Scene> m_Context;
Entity m_SelectionContext;
};
}SceneHierarchyPanel.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46namespace HEngine
{
SceneHierarchyPanel::SceneHierarchyPanel(const Ref<Scene>& context)
{
SetContext(context);
}
void SceneHierarchyPanel::SetContext(const Ref<Scene>& context)
{
m_Context = context;
}
void SceneHierarchyPanel::OnImGuiRender()
{
ImGui::Begin("Scene Hierarchy");
for (auto entity : m_Context->m_Registry.view<entt::entity>())
{
DrawEntityNode( { entity, m_Context.get() });
}
ImGui::End();
}
void SceneHierarchyPanel::DrawEntityNode(Entity entity)
{
auto& tag = entity.GetComponent<TagComponent>().Tag;
/*
ImGuiTreeNodeFlags_Selected:如果选中这个实体,则高亮这个实体。
ImGuiTreeNodeFlags_OpenOnArrow:使节点可以点击箭头展开,而不是整个区域都能展开。
*/
ImGuiTreeNodeFlags flags = ((m_SelectionContext == entity) ? ImGuiTreeNodeFlags_Selected : 0) | ImGuiTreeNodeFlags_OpenOnArrow;
//TreeNodeEx 用于绘制一个可展开的树节点。
bool opened = ImGui::TreeNodeEx((void*)(uint64_t)(uint32_t)entity, flags, tag.c_str());
if (ImGui::IsItemClicked())
{
m_SelectionContext = entity;
}
if (opened)//该节点是否被展开
{
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow;
bool opened = ImGui::TreeNodeEx((void*)888888, flags, tag.c_str());
if (opened)
ImGui::TreePop();
ImGui::TreePop();
}
}
}
五十四、添加属性面板
前言
此节目的
写出像Unity点击Cube,显示的Inspector面板,上面显示Cube的所有组件:transform、Mesh等。
代码流程
如果当前点击的实体有效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39void SceneHierarchyPanel::OnImGuiRender()
{
// 优化:若当前在hierarchy面板并且没点击到实体,属性面板清空
if (ImGui::IsMouseDown(0) && ImGui::IsWindowHovered()) {
m_SelectionContext = {};
}
//新增面板
ImGui::Begin("Properties");
if (m_SelectionContext) {
DrawComponents(m_SelectionContext);
}
ImGui::End();
}
void SceneHierarchyPanel::DrawComponents(Entity entity)
{
if (entity.HasComponent<TagComponent>())
{
auto& tag = entity.GetComponent<TagComponent>().Tag;
char buffer[256];
memset(buffer, 0, sizeof(buffer));
strcpy_s(buffer, sizeof(buffer), tag.c_str());
if (ImGui::InputText("Tag", buffer, sizeof(buffer)))
{
tag = std::string(buffer);
}
}
if (entity.HasComponent<TransformComponent>())
{
if (ImGui::TreeNodeEx((void*)typeid(TransformComponent).hash_code(), ImGuiTreeNodeFlags_DefaultOpen, "Transform"))
{
auto& transform = entity.GetComponent<TransformComponent>().Transform;
ImGui::DragFloat3("Position", glm::value_ptr(transform[3]), 0.1f);
ImGui::TreePop();
}
}
}
五十五、摄像头 UI
前言
- 此节目的
- 在属性面板上实现点击摄像机实体,属性面板显示摄像机实体的摄像机组件参数。
- 修改摄像机的参数,摄像机会做出相应的显示。
- 给摄像机添加透视投影类型。
代码
SceneHierarchyPanel.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64void SceneHierarchyPanel::DrawComponents(Entity entity)
{
if (entity.HasComponent<CameraComponent>())
{
if (ImGui::TreeNodeEx((void*)typeid(CameraComponent).hash_code(), ImGuiTreeNodeFlags_DefaultOpen, "Camera"))
{
auto& cameraComponent = entity.GetComponent<CameraComponent>();
auto& camera = cameraComponent.Camera;
ImGui::Checkbox("Primary", &cameraComponent.Primary);
const char* projectionTypeStrings[] = { "Perspective", "Orthographic" };
const char* currentProjectionTypeString = projectionTypeStrings[(int)camera.GetProjectionType()];
if (ImGui::BeginCombo("Projection", currentProjectionTypeString))
{
for (int i = 0; i < 2; i++)
{
bool isSelected = currentProjectionTypeString == projectionTypeStrings[i];
if (ImGui::Selectable(projectionTypeStrings[i], isSelected))
{
currentProjectionTypeString = projectionTypeStrings[i];
//设置摄像头类型,重新计算投影矩阵
camera.SetProjectionType((SceneCamera::ProjectionType)i);
}
if (isSelected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (camera.GetProjectionType() == SceneCamera::ProjectionType::Perspective)
{
float verticalFov = glm::degrees(camera.GetPerspectiveVerticalFOV());
if (ImGui::DragFloat("Vertical FOV", &verticalFov))
camera.SetPerspectiveVerticalFOV(glm::radians(verticalFov));
float orthoNear = camera.GetPerspectiveNearClip();
if (ImGui::DragFloat("Near", &orthoNear))
camera.SetPerspectiveNearClip(orthoNear);
float orthoFar = camera.GetPerspectiveFarClip();
if (ImGui::DragFloat("Far", &orthoFar))
camera.SetPerspectiveFarClip(orthoFar);
}
if (camera.GetProjectionType() == SceneCamera::ProjectionType::Orthographic)
{
float orthoSize =camera.GetOrthographicSize();
if (ImGui::DragFloat("Size", &orthoSize))
camera.SetOrthographicSize(orthoSize);
float orthoNear = camera.GetOrthographicNearClip();
if (ImGui::DragFloat("Near", &orthoNear))
camera.SetOrthographicNearClip(orthoNear);
float orthoFar = camera.GetOrthographicFarClip();
if (ImGui::DragFloat("Far", &orthoFar))
camera.SetOrthographicFarClip(orthoFar);
ImGui::Checkbox("Fixed Aspect Ratio", &cameraComponent.FixedAspectRatio);
}
}
}
}在Scene中绘制时,获取到主摄像机的投影矩阵,计算得到投影视图矩阵传入给OpenGL计算绘制的图形在世界的最终位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void SceneCamera::RecalculateProjection()
{
// 区分是正交还是透视
if (m_ProjectionType == ProjectionType::Perspective)
{
//glm::perspective()计算透视投影矩阵
m_Projection = glm::perspective(m_PerspectiveFOV, m_AspectRatio, m_PerspectiveNear, m_PerspectiveFar);
}
else
{
float orthoLeft = -m_OrthographicSize * m_AspectRatio * 0.5f;
float orthoRight = m_OrthographicSize * m_AspectRatio * 0.5f;
float orthoBottom = -m_OrthographicSize * 0.5f;
float orthoTop = m_OrthographicSize * 0.5f;
m_Projection = glm::ortho(orthoLeft, orthoRight, orthoBottom,
orthoTop, m_OrthographicNear, m_OrthographicFar);
}
}
五十六、Sprite UI
前言
此节目的
点击实体,在属性面板里显示这个实体的spriterenderer组件的属性,应包括颜色、texture、shader什么的(纹理),但目前先只显示颜色
代码
SceneHierarchyPhanel.cpp
1 | // 实体transform组件 |
五十七、TransForm UI
前言
此节目的
点击实体,在属性面板显示实体的Transform组件的位置、缩放、旋转属性。
对UI有要求
- 文字需在左边,属性按钮在右边
- x不是label,而是按钮且带有颜色,且点击按钮可以复原值。
代码
Components.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19struct TransformComponent
{
glm::vec3 Translation = { 0.0f, 0.0f, 0.0f };
glm::vec3 Rotation = { 0.0f, 0.0f,0.0f };
glm::vec3 Scale = { 1.0f, 1.0f, 1.0f };
TransformComponent(const glm::vec3& translation)
: Translation(translation) {}
glm::mat4 GetTransform()const {
glm::mat4 rotation = glm::rotate(glm::mat4(1.0f), Rotation.x, { 1,0,0 })
* glm::rotate(glm::mat4(1.0f), Rotation.y, { 0, 1, 0 })
* glm::rotate(glm::mat4(1.0f), Rotation.z, { 0, 0, 1 });
return glm::translate(glm::mat4(1.0f), Translation)
* rotation
* glm::scale(glm::mat4(1.0f), Scale);
}
};SceneHierarchyPanel.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74static void DrawVec3Control(const std::string& label, glm::vec3& values, float resetValue = 0.0f, float columnWidth = 100.0f)
{
ImGui::PushID(label.c_str()); // 为每个控件组推入唯一ID,防止命名冲突
ImGui::Columns(2); // 设置两列布局
ImGui::SetColumnWidth(0, columnWidth); // 第一列的宽度
ImGui::Text(label.c_str()); // 显示标签
ImGui::NextColumn(); // 切换到第二列
ImGui::PushMultiItemsWidths(3, ImGui::CalcItemWidth()); // 设置 3 个元素的宽度
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2{ 0, 0 }); // 设置控件间距为 0
// 计算按钮大小(基于字体大小)
float lineHeight = GImGui->Font->FontSize + GImGui->Style.FramePadding.y * 2.0f;
ImVec2 buttonSize = { lineHeight + 3.0f, lineHeight };
// X 轴按钮(红色)
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{ 0.8f, 0.1f, 0.15f, 1.0f });
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{ 0.9f, 0.2f, 0.2f, 1.0f });
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4{ 0.8f, 0.1f, 0.15f, 1.0f });
if (ImGui::Button("X", buttonSize)) // 点击按钮时重置 X 值
values.x = resetValue;
ImGui::PopStyleColor(3); // 恢复按钮颜色
ImGui::SameLine(); // 让 X 轴拖动条与按钮在同一行
ImGui::DragFloat("##X", &values.x, 0.1f, 0.0f, 0.0f, "%.2f"); // 拖动条,调整 X 值
ImGui::PopItemWidth(); // 弹出 X 轴控件宽度
ImGui::SameLine(); // 让 Y 轴按钮在同一行
// Y 轴按钮(绿色)
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{ 0.2f, 0.7f, 0.2f, 1.0f });
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{ 0.3f, 0.8f, 0.3f, 1.0f });
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4{ 0.2f, 0.7f, 0.2f, 1.0f });
if (ImGui::Button("Y", buttonSize))
values.y = resetValue;
ImGui::PopStyleColor(3);
ImGui::SameLine();
ImGui::DragFloat("##Y", &values.y, 0.1f, 0.0f, 0.0f, "%.2f");
ImGui::PopItemWidth();
ImGui::SameLine();
// Z 轴按钮(蓝色)
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{ 0.1f, 0.25f, 0.8f, 1.0f });
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4{ 0.2f, 0.35f, 0.9f, 1.0f });
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4{ 0.1f, 0.25f, 0.8f, 1.0f });
if (ImGui::Button("Z", buttonSize))
values.z = resetValue;
ImGui::PopStyleColor(3);
ImGui::SameLine();
ImGui::DragFloat("##Z", &values.z, 0.1f, 0.0f, 0.0f, "%.2f");
ImGui::PopItemWidth();
ImGui::PopStyleVar(); // 恢复原始的 ItemSpacing
ImGui::Columns(1); // 结束列布局
ImGui::PopID(); // 弹出 ID,防止影响其他控件
}
void SceneHierarchyPanel::DrawComponents(Entity entity)
{
if (entity.HasComponent<TransformComponent>())
{
auto& tc = entity.GetComponent<TransformComponent>();
DrawVec3Control("Translation", tc.Translation);
glm::vec3 rotation = glm::degrees(tc.Rotation);
DrawVec3Control("Rotation", rotation);
tc.Rotation = glm::radians(rotation);
DrawVec3Control("Scale", tc.Scale, 1.0f);
}
}
五十八、实体和组件增删的UI
前言
目的
此节需要完成像Unity那样
- 右键实体可以弹出菜单可以删除实体,右键空白地方可以弹出菜单添加实体
- 在属性面板显示实体的组件,有添加组件按钮,点击弹出菜单项可以添加相应组件。
- 在属性面板显示实体的组件,每个组件的下拉有按钮,点击弹出菜单项可以删除这个组件
代码
删除、添加实体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41void SceneHierarchyPanel::OnImGuiRender()
{
// 判断当前点击的实体是否存在
ImGui::Begin("Properties");
if (m_SelectionContext) { // operator uint32_t() 的是const,不然不会调用operator bool(),而是调用uint32_t()
DrawComponents(m_SelectionContext);
// 弹出菜单: 在属性面板显示添加组件按钮
if (ImGui::Button("Add Component")) {
ImGui::OpenPopup("AddComponent");// AddComponent只是id
}
if (ImGui::BeginPopup("AddComponent")) {
if (ImGui::MenuItem("Camera")) {
m_SelectionContext.AddComponent<CameraComponent>();
ImGui::CloseCurrentPopup();
}
if (ImGui::MenuItem("Sprite Renderer")) {
m_SelectionContext.AddComponent<SpriteRendererComponent>();
}
ImGui::EndPopup();
}
}
ImGui::End();
}
void SceneHierarchyPanel::DrawEntityNode(Entity entity)
{
auto& tag = entity.GetComponent<TagComponent>().Tag;
// 若是被点击标记为选中状态|有下一级
ImGuiTreeNodeFlags flags = ((m_SelectionContext == entity) ? ImGuiTreeNodeFlags_Selected : 0) | ImGuiTreeNodeFlags_OpenOnArrow;
// 第一个参数是唯一ID 64的,
bool opened = ImGui::TreeNodeEx((void*)(uint64_t)(uint32_t)entity, flags, tag.c_str());
if (ImGui::IsItemClicked()) {
m_SelectionContext = entity; // 记录当前点击的实体
}
// 右键实体 弹出菜单:删除实体
bool entityDeleted = false;
if (ImGui::BeginPopupContextItem()) {
if (ImGui::MenuItem("Delete Entity")) {
entityDeleted = true;
}
ImGui::EndPopup();
}ImGui弹出菜单与延迟删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76void SceneHierarchyPanel::OnImGuiRender()
{
if (!m_SelectionContext) //When item is not selected
{
// 右击空白面板-弹出菜单。0是ID 1是右键
if (ImGui::BeginPopupContextWindow(0, 1, false)) {
if (ImGui::MenuItem("Create Empty Entity")) {
m_Context->CreateEntity("Empty Entity");
}
ImGui::EndPopup();
}
}
if (m_SelectionContext)
{
if (ImGui::Button("Add Component"))
ImGui::OpenPopup("AddComponent");
if (ImGui::BeginPopup("AddComponent"))
{
if (ImGui::MenuItem("Camera"))
{
m_SelectionContext.AddComponent<CameraComponent>();
ImGui::CloseCurrentPopup();
}
if (ImGui::MenuItem("Sprite Renderer"))
{
m_SelectionContext.AddComponent<SpriteRendererComponent>();
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
}
void SceneHierarchyPanel::DrawEntityNode(Entity entity)
{
bool entityDeleted = false;
if (ImGui::BeginPopupContextItem()) //选中Item并右键触发
{
if (ImGui::MenuItem("Delete Entity"))
entityDeleted = true;
ImGui::EndPopup();
}
if (entityDeleted)
{
m_Context->DestoryEntity(entity);
if (m_SelectionContext == entity)
m_SelectionContext = {};
}
}
if (entity.HasComponent<SpriteRendererComponent>())
{
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{ 4, 4 });
bool open = ImGui::TreeNodeEx((void*)typeid(SpriteRendererComponent).hash_code(), treeNodeFlags, "Sprite Renderer");
ImGui::SameLine(ImGui::GetWindowWidth() - 25.0f);
if (ImGui::Button("+", ImVec2{ 20, 20 }))
{
ImGui::OpenPopup("ComponentSettings");
}
ImGui::PopStyleVar();
bool removeComponent = false;
if (ImGui::BeginPopup("ComponentSettings"))
{
if (ImGui::MenuItem("Remove component"))
removeComponent = true;
ImGui::EndPopup();
}
if (removeComponent)
entity.RemoveComponent<SpriteRendererComponent>();
}添加摄像机组件的时候,设置视口大小计算投影矩阵
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19Scene.h
private:
template<typename T>
void OnComponentAdded(Entity entity, T& component);
Scene.cpp
// 模板类定义在cpp中
template<typename T>
void Scene::OnComponentAdded(Entity entity, T& component)
{
// 静态断言:false,代表在编译前就会执行, 但是编译器这里不会报错,说明这段代码不会编译吧。。
// 而且打了断点,也不行,证明这段代码只是声明作用吧。
static_assert(false);
}
template<>
void Scene::OnComponentAdded<CameraComponent>(Entity entity, CameraComponent& component)
{
entity.GetComponent<TransformComponent>().Translation = { 0, 0, 5.0f };
component.camera.SetViewportSize(m_ViewportWidth, m_ViewportHeight);
}
五十九、新的主题外观
前言
此节目的
如此节标题,让编辑器更好看,主要有:
- 更换字体
- 按钮字体加粗
- 调整布局
- 控制最小宽度
- 界面颜色
- 用模板函数去除重复代码
关键代码
ImGui相关
更换字体
1
2
3// 完善UI:设置界面主题字体
io.Fonts->AddFontFromFileTTF("assets/fonts/opensans/OpenSans-Bold.ttf", 18.0f);
io.FontDefault = io.Fonts->AddFontFromFileTTF("assets/fonts/opensans/OpenSans-Regular.ttf", 18.0f);按钮字体加粗
1
2
3
4
5
6
7// 完善UI:加粗字体
ImGui::PushFont(boldFont);
if (ImGui::Button("X", buttonSize) ){
values.x = resetValue;
}
ImGui::PopFont();
ImGui::PopStyleColor(3);控制最小宽度
1
2
3
4
5
6
7
8
9// 完善UI:设置面板最小宽度
float minWinSizeX = style.WindowMinSize.x;
style.WindowMinSize.x = 370.f;
if (io.ConfigFlags & ImGuiConfigFlags_DockingEnable)
{
ImGuiID dockspace_id = ImGui::GetID("MyDockSpace");
ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), dockspace_flags);
}
style.WindowMinSize.x = minWinSizeX; // 恢复界面颜色
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32void ImGuiLayer::SetDarkThemeColors()
{
auto& colors = ImGui::GetStyle().Colors;
colors[ImGuiCol_WindowBg] = ImVec4{ 0.1f, 0.105f, 0.11f, 1.0f };
// Headers
colors[ImGuiCol_Header] = ImVec4{ 0.2f, 0.205f, 0.21f, 1.0f };
colors[ImGuiCol_HeaderHovered] = ImVec4{ 0.3f, 0.305f, 0.31f, 1.0f };
colors[ImGuiCol_HeaderActive] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };
// Buttons
colors[ImGuiCol_Button] = ImVec4{ 0.2f, 0.205f, 0.21f, 1.0f };
colors[ImGuiCol_ButtonHovered] = ImVec4{ 0.3f, 0.305f, 0.31f, 1.0f };
colors[ImGuiCol_ButtonActive] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };
// Frame BG
colors[ImGuiCol_FrameBg] = ImVec4{ 0.2f, 0.205f, 0.21f, 1.0f };
colors[ImGuiCol_FrameBgHovered] = ImVec4{ 0.3f, 0.305f, 0.31f, 1.0f };
colors[ImGuiCol_FrameBgActive] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };
// Tabs
colors[ImGuiCol_Tab] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };
colors[ImGuiCol_TabHovered] = ImVec4{ 0.38f, 0.3805f, 0.381f, 1.0f };
colors[ImGuiCol_TabActive] = ImVec4{ 0.28f, 0.2805f, 0.281f, 1.0f };
colors[ImGuiCol_TabUnfocused] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };
colors[ImGuiCol_TabUnfocusedActive] = ImVec4{ 0.2f, 0.205f, 0.21f, 1.0f };
// Title
colors[ImGuiCol_TitleBg] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };
colors[ImGuiCol_TitleBgActive] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };
colors[ImGuiCol_TitleBgCollapsed] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f };
}
用模板函数去除重复代码
SceneHierarchyPanel.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66template<typename T, typename UIFunction>
static void DrawComponent(const std::string& name, Entity entity, UIFunction uiFunction)
{
const ImGuiTreeNodeFlags treeNodeFlags = ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_AllowItemOverlap | ImGuiTreeNodeFlags_FramePadding;
if (entity.HasComponent<T>())
{
auto& component = entity.GetComponent<T>();
ImVec2 contentRegionAvailable = ImGui::GetContentRegionAvail();
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{ 4, 4 });
float lineHeight = GImGui->Font->FontSize + GImGui->Style.FramePadding.y * 2.0f;
ImGui::Separator();
bool open = ImGui::TreeNodeEx((void*)typeid(T).hash_code(), treeNodeFlags, name.c_str());
ImGui::PopStyleVar();
ImGui::SameLine(contentRegionAvailable.x - lineHeight * 0.5f);
if (ImGui::Button("+", ImVec2{ lineHeight, lineHeight }))
{
ImGui::OpenPopup("ComponentSettings");
}
bool removeComponent = false;
if (ImGui::BeginPopup("ComponentSettings"))
{
if (ImGui::MenuItem("Remove component"))
removeComponent = true;
ImGui::EndPopup();
}
if (open)
{
uiFunction(component);
ImGui::TreePop();
}
if (removeComponent)
entity.RemoveComponent<T>();
}
}
-------------------------------------
DrawComponent<TransformComponent>("Transform", entity, [](auto& component)
{
DrawVec3Control("Translation", component.Translation);
glm::vec3 rotation = glm::degrees(component.Rotation);
DrawVec3Control("Rotation", rotation);
component.Rotation = glm::radians(rotation);
DrawVec3Control("Scale", component.Scale, 1.0f);
});
DrawComponent<CameraComponent>("Camera", entity, [](auto& component)
{
auto& camera = component.Camera;
ImGui::Checkbox("Primary", &component.Primary);
const char* projectionTypeStrings[] = { "Perspective", "Orthographic" };
const char* currentProjectionTypeString = projectionTypeStrings[(int)camera.GetProjectionType()];
@@ -256,41 +298,13 @@ namespace HEngine
if (ImGui::DragFloat("Far", &orthoFar))
camera.SetOrthographicFarClip(orthoFar);
ImGui::Checkbox("Fixed Aspect Ratio", &component.FixedAspectRatio);
}
});
...
六十、 保存和加载场景
前言
目的
为了实现像unity那样保存场景到本地和从本地加载场景的功能,这功能其实就是将场景的实体和组件用文本信息保存起来,加载场景的话就是读取文本信息,根据文本存储的信息,创建实体并且设置组件和组件数据。
将yaml项目作为子模块融入项目中
cmd
1
git submodule add https://github.com/jbeder/yaml-cpp
打开yaml-cpp文件夹下的premake.lua,将staticruntime “off”改为
1
staticruntime "on"
目的是使yaml-cpp项目作为静态链接
关键的代码
存储
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36void SceneSerializer::Serialize(const std::string& filepath)
{
YAML::Emitter out;
out << YAML::BeginMap;
out << YAML::Key << "Scene" << YAML::Value << "Untitled";
out << YAML::Key << "Entities" << YAML::Value << YAML::BeginSeq;// 开始序列化
m_Scene->m_Registry.each([&](auto entityID) {
Entity entity = { entityID ,m_Scene.get() };
if (!entity)
return;
// 序列化实体
SerializeEntity(out, entity);
});
out << YAML::EndSeq; // 结束序列化
out << YAML::EndMap;
std::ofstream fout(filepath);
fout << out.c_str();
}
static void SerializeEntity(YAML::Emitter& out, Entity entity) {
out << YAML::BeginMap;
out << YAML::Key << "Entity" << YAML::Value << "16816816816888";
if (entity.HasComponent<TagComponent>()) {
out << YAML::Key << "TagComponent";
out << YAML::BeginMap;
auto& tag = entity.GetComponent<TagComponent>().Tag;
out << YAML::Key << "Tag" << YAML::Value << tag;
out << YAML::EndMap;
}
// 其它组件类似
out << YAML::EndMap;
}解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37std::ifstream stream(filepath);
std::stringstream strStream;
strStream << stream.rdbuf(); // strStream流对象中的流重定向到字符串输出流strStream
// 转换为node对象
YAML::Node data = YAML::Load(strStream.str());
if (!data["Scene"]) {
return false;
}
std::string sceneName = data["Scene"].as<std::string>();
HE_CORE_TRACE("Deserializing scene '{0}'", sceneName);
auto entities = data["Entities"];
if (entities) {
for (auto entity : entities) {
uint64_t uuid = entity["Entity"].as<uint64_t>();
std::string name;
auto tagComponent = entity["TagComponent"];
if (tagComponent) {
name = tagComponent["Tag"].as<std::string>();
}
HE_CORE_TRACE("Deserialized entity with ID = {0}, name = {1}", uuid, name);
Entity deserializedEntity = m_Scene->CreateEntity(name);;
auto transformComponent = entity["TransformComponent"];
if (transformComponent) {
// 添加实体,默认有transform组件
auto& tc = deserializedEntity.GetComponent<TransformComponent>();
tc.Translation = transformComponent["Translation"].as<glm::vec3>();
tc.Rotation = transformComponent["Rotation"].as<glm::vec3>();
tc.Scale = transformComponent["Scale"].as<glm::vec3>();
}
.....
存储的文件
1
2
3
4
5
6
7
8
9Scene: Untitled
Entities:
- Entity: 16816816816888
TagComponent:
Tag: Camera B
- Entity: 16816816816888
TagComponent:
Tag: Camera A
......
关于yaml的BeginSeq
开了<< YAML::BeginSeq
1 | Scene: Untitled |
没开<< YAML::BeginSeq
1 | Scene: Untitled |
可以看出,BeginSeq是在实体信息前加一个“-”,代表着是一个实例