1. Application
这里的分析主要参考Urho3D的两个例子HelloWorld和StaticScene,前者使用UI组件,用于分析UI子系统,后者显示模型,用于分析Renderer子系统。
1.1 URHO3D_DEFINE_APPLICATION_MAIN()
URHO3D_DEFINE_APPLICATION_MAIN()宏用于创建唯一的application实例。下面是Urho3D的例子HelloWorld。
URHO3D_DEFINE_APPLICATION_MAIN(HelloWorld)
展开后的代码如下:
int RunApplication()
{
Urho3D::SharedPtr context(new Urho3D::Context());
Urho3D::SharedPtr application(new HelloWorld(context));
return application->Run();
}
int main(int argc, char** argv)
{
Urho3D::ParseArguments(argc, argv);
return RunApplication();
}
HelloWorld从Application派生,这里创建HelloWorld的实例,并调用Application::Run()启动它。在创建application之前,先创建公共的Context实例,以便传给Application,并在之后再传给其他类。
1.2 定制Application
Application定义了一组虚拟函数Setup()、Start()和Stop()。使用者可以在自己的Applicatio类中定制自己的实现。一个典型的做法是在Start()中创建UI组件和模型组件。这样UI子系统和Renderer系统可以分别对它们进行操作。
下图是main()的调用步骤。
- 创建Context实例。
- 创建使用者的Application类实例。
- 在Application::Run()中调用使用者实现的Setup()、Start()和Stop()
- 在Application()中创建Engine实例,在Application::Run()调用Engine::Initialize()初始化,在调用Engine::RunFrame()驱动它。Engine组合了对UI和Renderer及其他子系统的功能。
2. Engine
2.1 Engine::Engine()
在构造函数Engine()中,初始化UI、FileSystem、Log、ResourceCache等子系统并向Context注册。在UI的构造函数UI()中,调用RegisterUILibrary()。
2.2 Engine::Initialize()
Engine::Initialize()的工作如下:
创建Graphics子系统和Renderer子系统,向Context注册。
-
调用Graphics::SetMode(),这将触发UI子系统和Renderer子系统的初始化。
- 调用SDL_CreateWindow()创建用于绘制的窗口。
- 发送E_SCREENMODE事件。UI在UI::HandleScreenMode()中处理该事件,调用Renderer::Initialize()。Renderer在Renderer::HandleScreenMode()中处理该事件,调用Renderer::Initialize()。
- 调用CheckFeatureSupport(),检查支持哪些OpenGL扩展选项。
设置Renderer。
2.3 Engine::runFrame()
Engine::RunFrame()依次调用Engine::Update()进行渲染前的准备工作,然后调用Engine::Render()渲染。
对于UI子系统,
- 发送的E_POSTUPDATE事件,由UI::HandlePostUpdate()处理,后者调用UI::Update()。
- 发送的E_RENDERUPDATE事件,由UI::HandleRenderUpdate()处理,其中调用UI::RenderUpdate()。
- 在Engine::Render()中,调用UI::Render()渲染UI组件。
对于Renderer子系统,
- 发送的E_RENDERUPDATE事件,由Renderer::HandleRenderUpdate()处理,其中调用Renderer::Update()。
- 在Engine::Render()中,调用Renderer::Render()渲染模型。
Engine中还包括Graphics的处理,Graphics负责绘制图像前的准备工作。
在Graphics::BeginFrame()中,
- 如调用glEnable()设置绘制选项。
- 调用glViewPort()设置绘制视口。
- 发送E_BEGINRENDERING事件。
在Graphics::EndFrame()中,
- 发送E_ENDRENDERING事件。
3. Graphics子系统
Graphics包括与显示编程接口有关的操作。如下列出了Graphics的部分成员函数。
函数 | 功能 | 调用的函数 |
---|---|---|
SetMode() | 创建窗口 | SDL_CreateWindow() |
Clear() | 清除背景 | glClearColor()、glClearDepth()、glClearStencil()、glClear() |
SetViewport() | 设置视口 | glViewport() |
SetBlendMode() | 设置混合模式 | glBlendFunc()、glBlendEquation() |
SetColorWrite() | 设置颜色mask | glColorMask() |
SetCullMode() | 设置剔除模式 | glCullFace() |
SetDepthTest() | 设置深度测试 | glDepthFunc() |
SetScissorTest() | 设置裁剪测试 | glScissor() |
SetStencilTest() | 设置模板测试 | glStencilFunc()、glStencilOp() |
SetShaders() | 设置Shader、Program | glCreateShader()、glShaderSource()、glCompileShader()、 |
glCreateProgram()、glAttachShader()、glLinkProgram、glUseProgram() | ||
SetShaderParameter() | 设置Shader的uniform变量 | glUniform1fv()、glUniformMatrix3fv() ... |
SetVBO() | 绑定顶点buffer | glBindBuffer() |
PrepareDraw() | 设置顶点数据 | glVertexAttribPointer() |
Draw() | 绘制 | glDrawArrays()、glDrawElements()、glDrawElementsInstanced() |
3.1 Shader & Program
3.1.1 Shader & Program概念
Shader是从资源文件加载的Resource,而ShaderVariation是从Shader生成的、OpenGL意义上的Shader。ShaderVariation的成员owner_指向引用的Shader实例。另外一个成员type_指出这是一个vertex shader还是一个pixel/fragment shader。
ShaderProgram类代表program。在构造函数中,它引用一个vertex ShaderVariation和一个pixel ShaderVariation。
ShaderProgram的成员vertexAttributes_保存了program的vertex attribute到其位置的映射关系。Vertex attribute的信息包括两部分,一是semantic,二是序号。所谓的semantic由数组ShaderVariation::elementSemanticNames[]指定:
const char* ShaderVariation::elementSemanticNames[] =
{
"POS",
"NORMAL",
"TEXCOORD",
};
如下是一个vertex attribute的例子。
attribute vec4 iPos;
attribute vec3 iNormal;
attribute vec4 iCubeTexCoord1;
通过检查attribute的名字是否包含elementSemanticNames[]中的semantic字符串,判断是那种semantic。如iPos包含POS,所以对应的semantic是POS。
Attribute可能附加了数字,如iCubeTexCoord1附加了1,这个数字就是序号。如果没有附加数字,则序号为-1。如iCubeTexCoord1的序号是1,iPos的序号是-1。
ShaderParameter保存了uniform变量的信息,type_是从属shader的类型,name_是变量名,location_是变量位置。ShaderProgram的成员shaderParameters_则保存了一般uniform变量(不包括sampler变量)的名称到ShaderParameter的映射关系。
3.1.2 创建Shader
ShaderVariation::Create()负责创建opengl意义上的shader。
- 调用Shader::GetSourceCode()得到Shader源代码。
- 源代码中使用了宏,以便指定其中某些代码有效或无效。使用ShaderVariantion::SetDefines()可以设置这些宏。下面的DIFFMAP是宏的一个例子。如果定义了DIFFMAP,则往代码中加入#define DIFFMAP,就是下面红色的一行。
#define DIFFMAP
void VS()
{
#ifdef DIFFMAP
vTexCoord = iTexCoord;
#endif
}
指定的宏不同,则从一个Shader实例创建的ShaderVariation不同。如果宏相同,就要避免重复创建ShaderVariation实例了。Shader把从宏到ShaderVariation实例的映射保存在成员vsVariantions_和psVariantions_中。
调用Shader::GetVariation()可以得到与指定宏对应的ShaderVariation,它的参数就是包含宏的字符串。
- NormalDefines()将字符串标准化,也就是把字符全部转成大写,将包含的多个宏重新按顺序排列。
- 如果对应的ShaderVariation还不存在,则创建ShaderVariation实例,并设置宏。后面调用ShaderVariation::Create()时就可以使用了。
- 如果ShaderVariation已经存在,则返回就好了。
3.1.3 创建Program
ShaderProgram::Link()创建Program,分三步。
一是调用glCreateProgram()和glAttachShader()创建program。
二是得到vertex attribute。
- 调用glGetProgramiv(GL_ACTIVE_ATTRIBUTES)得到所有vertex attribute,并遍历。
- 调用glActiveAttrib()得到vertex attribute。调用glGetAttribLocation()得到attribute的位置。根据vertex attribute的名字查询ShaderVariation::elementSemanticNames[]数组,得到其semantic。将attribute的信息(samantic, 序号)到位置的映射关系保存在成员vertexAttributes_中。
三是得到uniform变量,包括sampler变量和一般的uniform变量。区分这两者办法是看前缀字符。sampler变量以s开头,如sDiffMap、sDiffCubeMap等。一般变量以c开头,如cModel、cViewProj等。
- 调用glGetProgramiv(GL_ACTIVE_UNIFORMS)得到所有vertex attribute,并遍历。
- 对于sampler变量,找到与变量对应的texture unit,调用glUniform1iv()将uniform变量绑定到texture unit上。
- 对于一般变量,将变量名和ShaderParameter的映射关系保存在成员shaderParameters_中。
3.1.4 Graphics与ShaderProgram
Graphics的成员impl_是一个GraphicsImpl实例,后者分担Graphics的部分功能。
GraphicsImpl的成员shaderProgram_持有唯一的ShaderProgram实例。
3.2 VertexBuffer
3.2.1 Vertex Buffer概念
GPUObject保存OpenGL对象的句柄。它的成员object_是一个union类型GPUObjectHandle,后者的成员name_保存的就是句柄值,如glGenBuffers()得到的buffer的句柄值。
VertexBuffer从GPUObject派生的,它保存一组顶点。VertexBuffer的每个顶点可以包括不同semantic的顶点数据,semantic由枚举类型VertexElementSemantic规定,如位置、法线向量等。
enum VertexElementSemantic
{
SEM_POSITION = 0,
SEM_NORMAL,
SEM_TEXCOORD,
};
VertexElement负责保存这些semantic信息。VertexBuffer的成员elements_就是一组VertexElement实例,如果VertexBuffer中有3种semantic数据,则elements_中就有3个VertexElement实例。
Vertexbuffer中数据的排列方式是每个顶点的数据放在一起,然后所有顶点排列成一个数组。
VertexElement除了semantic_之外的其他成员:
- type_是数据类型,可以通过查询数组ELEMENT_TYPESIZE[]得到它的字段大小。
URHO3D_API const unsigned ELEMENT_TYPESIZES[] =
{
sizeof(int),
sizeof(float),
}
- offset_是该semantic在vertexbuffer的数据中的偏移位置。
3.2.2 VertexBuffer::SetSize()
有两层VertexBuffer::SetSize()。
- 顶层SetSize()的参数是一个位掩码,指定有哪些vertex element。调用GetElements(),在数组LEGACY_VERTEXELEMENTS[]查找每个位掩码对应的VertexElement,并保存在VertexBuffer的成员elements_中。注意这时的VertexElement只有数据类型和semantic,而offset_等其他成员没有设置。
extern URHO3D_API const VertexElement LEGACY_VERTEXELEMENTS[] =
{
VertexElement(TYPE_VECTOR3, SEM_POSITION, 0, false), // Position
VertexElement(TYPE_VECTOR3, SEM_NORMAL, 0, false), // Normal
VertexElement(TYPE_UBYTE4_NORM, SEM_COLOR, 0, false), // Color
}
- 调用下层VertexBuffer::SetSize()。其中,
- 调用UpdateOffsets()。遍历elements_,设置VertexElement的成员offset_。
- 调用Create()。其中调用glGenBuffers()生成buffer,调用glBindBuffer()绑定buffer,调用glBufferData()写入顶点数据,不过这时顶点数据为空。
3.2.3 VertexBuffer::SetData()
VertexBuffer::SetData()的工作是:
调用glBindBuffer()绑定Buffer,调用glBufferData()写入顶点数据,这里顶点数据不为空。
相关链接
Urho3D 1.7.1 源代码分析 (一)
Urho3D 1.7.1 源代码分析 (二)
Urho3D 1.7.1 源代码分析 (三)
Urho3D 1.7.1 源代码分析 (四)
Urho3D 1.7.1 源代码分析 (五)