2021SC@SDUSC
目录
Loaders
1.ModelLoader
1.1Create
1.2Reload
1.3Destroy?
2.ShaderLoader
2.1CreateProgram
2.2CompileShader
2.3ParseShader
上一节我们分析了Parser的相关代码,这一节就来看与资源导入相关的另一类工具——Loader。
Loaders
1.ModelLoader
首先是ModelLoader,它的作用是处理模型的创建和销毁,我们来看具体的代码
class ModelLoader
{
public:
ModelLoader() = delete;
/**
* Create a model
* @param p_filepath
* @param p_parserFlags
*/
static Model* Create(const std::string& p_filepath, Parsers::EModelParserFlags p_parserFlags = Parsers::EModelParserFlags::NONE);
/**
* Reload a model from file
* @param p_model
* @param p_filePath
* @param p_parserFlags
*/
static void Reload(Model& p_model, const std::string& p_filePath, Parsers::EModelParserFlags p_parserFlags = Parsers::EModelParserFlags::NONE);
/**
* Disabled constructor
* @param p_modelInstance
*/
static bool Destroy(Model*& p_modelInstance);
private:
static Parsers::AssimpParser __ASSIMP;
};
可以看到这个类中除了不可用的构造函数外,还包含了一个创建函数、一个重载函数以及一个销毁函数,同时引入了一个AssimpParser对象。
1.1Create
在create函数中,我们会用到model类的内容,相关的内容会在下一节讲解。
从该函数中我们看到它只需要2个参数,一个是模型文件的路径,一个是我们上一节讲过的EModelParserFlags枚举值。最开始我们创建一个Model对象的指针,并用传入文件路径参数做初始化,这个指针将会方便我们指向对应模型参数。
OvRendering::Resources::Model* OvRendering::Resources::Loaders::ModelLoader::Create(const std::string& p_filepath, Parsers::EModelParserFlags p_parserFlags)
{
Model* result = new Model(p_filepath);
if (__ASSIMP.LoadModel(p_filepath, result->m_meshes, result->m_materialNames, p_parserFlags))
{
result->ComputeBoundingSphere();
return result;
}
delete result;
return nullptr;
}
接下来使用AssimpParser对象的LoaderModel函数加载模型信息,这里会用到model对象的参数,如果模型加载成功将会返回true,就证明当前的model对象是可用的,我们就可以用它计算当前模型的碰撞盒信息,然后将其指针返回;如果模型加载失败,就意味没有模型导入,将会返回空指针。
1.2Reload
reload函数用于重载一个模型信息,说实话这一步的作用有点迷,我们需要从外界传入一个model对象的引用,然后利用create函数创建一个newModel,并将newModel的网格信息(m_meshes)、材质信息(m_materialNames)与碰撞球信息(m_boundingSphere)赋值给最开始传入函数的model对象,最后清空newModel的信息并销毁。
void OvRendering::Resources::Loaders::ModelLoader::Reload(Model& p_model, const std::string& p_filePath, Parsers::EModelParserFlags p_parserFlags)
{
Model* newModel = Create(p_filePath, p_parserFlags);
if (newModel)
{
p_model.m_meshes = newModel->m_meshes;
p_model.m_materialNames = newModel->m_materialNames;
p_model.m_boundingSphere = newModel->m_boundingSphere;
newModel->m_meshes.clear();
delete newModel;
}
}
怎么说呢,这个函数的作用应该为了实现资源的复用,通过不断更换一个指针指向的模型信息来简化外界的流程,所以需要使用一个newModel做中间过渡,及时销毁无用的对象。
1.3Destroy?
这个函数没什么可讲的内容,就是在判断当前实例为真的情况下删除对应的model对象。
bool OvRendering::Resources::Loaders::ModelLoader::Destroy(Model*& p_modelInstance)
{
if (p_modelInstance)
{
delete p_modelInstance;
p_modelInstance = nullptr;
return true;
}
return false;
}
2.ShaderLoader
ShaderLoader与ModelLoader类似,只不过它作用的对象的着色器,同时它的内容也会更加复杂。
class ShaderLoader
{
public:
/**
* Disabled constructor
*/
ShaderLoader() = delete;
/**
* Create a shader
* @param p_filePath
*/
static Shader* Create(const std::string& p_filePath);
/**
* Create a shader from source
* @param p_vertexShader
* @param p_fragmentShader
*/
static Shader* CreateFromSource(const std::string& p_vertexShader, const std::string& p_fragmentShader);
/**
* Recompile a shader
* @param p_shader
* @param p_filePath
*/
static void Recompile(Shader& p_shader, const std::string& p_filePath);
/**
* Destroy a shader
* @param p_shader
*/
static bool Destroy(Shader*& p_shader);
private:
static std::pair<std::string, std::string> ParseShader(const std::string& p_filePath);
static uint32_t CreateProgram(const std::string& p_vertexShader, const std::string& p_fragmentShader);
static uint32_t CompileShader(uint32_t p_type, const std::string& p_source);
static std::string __FILE_TRACE;
};
2.1CreateProgram
CreateProgram将会最经常用的的函数,它的作用是为我们定义的着色器生成一个着色器程序对象。
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。
这是一个非常工具化的函数,它只需要顶点着色器(p_vertexShader)与片段着色器(p_vertexShader)的对象作为参数。
首先我们需要调用OpenGL内置的函数创建一个着色器程序,该函数会返回program的id,同时使用CompileShader函数(该函数同样是ShaderLoader的内容,马上就会讲到)编译顶点着色器与片段着色器,该函数同样会返回着色器的id。
uint32_t OvRendering::Resources::Loaders::ShaderLoader::CreateProgram(const std::string& p_vertexShader, const std::string& p_fragmentShader)
{
const uint32_t program = glCreateProgram();
const uint32_t vs = CompileShader(GL_VERTEX_SHADER, p_vertexShader);
const uint32_t fs = CompileShader(GL_FRAGMENT_SHADER, p_fragmentShader);
然后判定两种着色器全部编译成功后,我们需要把之前编译的着色器附加到程序对象上,然后用glLinkProgram链接它们。
if (vs == 0 || fs == 0)
return 0;
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
在这里我们需要检测链接着色器程序是否失败,并获取相应的日志,我们使用的是OpenGL的glGetProgramiv函数。如果连接着色器失败了,linkStatus 会返回false,我们就需要使用OvDebug/Utils/Logger.h中的函数获得错误日志。
GLint linkStatus;
glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE)
{
GLint maxLength;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &maxLength);
std::string errorLog(maxLength, ' ');
glGetProgramInfoLog(program, maxLength, &maxLength, errorLog.data());
OVLOG_ERROR("[LINK] \"" + __FILE_TRACE + "\":\n" + errorLog);
glDeleteProgram(program);
return 0;
}
如果链接成功,我们就可以激活当前的着色器程序,并将着色器对象删除,因为对应的着色器信息已经链接在了着色器程序上。这里使用的都是OpenGL的函数。最后会将着色器程序的id返回。
glValidateProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
return program;
}
2.2CompileShader
接下来是上文曾经用过的CompileShader函数,这同样是一个封装流程的工具化函数,过程与CreateProgram有很多类似之处,在此我们需要两个参数:着色器类型与着色器路径。
我们首先要做的是创建一个着色器对象,注意还是用ID来引用的。所以我们储存这个顶点着色器为unsigned int,然后用glCreateShader创建这个着色器。
下一步我们把这个着色器源码附加到着色器对象上,然后编译它。glShaderSource函数把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为null。这里因为传入的路径是字符串,所以需要先将它转化为字符数组。
uint32_t OvRendering::Resources::Loaders::ShaderLoader::CompileShader(uint32_t p_type, const std::string& p_source)
{
const uint32_t id = glCreateShader(p_type);
const char* src = p_source.c_str();
glShaderSource(id, 1, &src, nullptr);
glCompileShader(id);
接下来的流程与CreateProgram类似,我们同样需要判断着色器是否编译成功,并将着色器的id返回。
GLint compileStatus;
glGetShaderiv(id, GL_COMPILE_STATUS, &compileStatus);
if (compileStatus == GL_FALSE)
{
GLint maxLength;
glGetShaderiv(id, GL_INFO_LOG_LENGTH, &maxLength);
std::string errorLog(maxLength, ' ');
glGetShaderInfoLog(id, maxLength, &maxLength, errorLog.data());
std::string shaderTypeString = p_type == GL_VERTEX_SHADER ? "VERTEX SHADER" : "FRAGMENT SHADER";
std::string errorHeader = "[" + shaderTypeString + "] \"";
OVLOG_ERROR(errorHeader + __FILE_TRACE + "\":\n" + errorLog);
glDeleteShader(id);
return 0;
}
return id;
}
2.3ParseShader
该函数是一个shader的解析器,它能从一个文件路径中读取对应的着色代码。
首先用输入文件流打开文件路径,然后定义着色器类型:顶点着色器=0,片段着色器=1,空=-1;然后定义一个字符串line访问文件中的字符串,以及一个字符串流数组用于存储着色器代码。
std::pair<std::string, std::string> OvRendering::Resources::Loaders::ShaderLoader::ParseShader(const std::string& p_filePath)
{
std::ifstream stream(p_filePath);
enum class ShaderType { NONE = -1, VERTEX = 0, FRAGMENT = 1 };
std::string line;
std::stringstream ss[2];
ShaderType type = ShaderType::NONE;
做好一切准备后,我们使用getline函数遍历文件中每一行的字符串,若变量line能访问到字符串#shader,则说明访问到了一个着色器代码。于是对line进行判断,若line中出现vertex,则表示访问到顶点着色器,并将type赋上对应的枚举值;若line中出现fragment也是同理。
若是line中没有字符串#shader,则有2种情况:当前位置位于着色器内或是位于着色器外。若是type不为空,则说明已进入着色器,并将着色器的代码输入字符串流数组的对应索引。这里需要将type的枚举值做强制类型转换,才能正确使用。
while (std::getline(stream, line))
{
if (line.find("#shader") != std::string::npos)
{
if (line.find("vertex") != std::string::npos) type = ShaderType::VERTEX;
else if (line.find("fragment") != std::string::npos) type = ShaderType::FRAGMENT;
}
else if (type != ShaderType::NONE)
{
ss[static_cast<int>(type)] << line << '\n';
}
}
return
{
ss[static_cast<int>(ShaderType::VERTEX)].str(),
ss[static_cast<int>(ShaderType::FRAGMENT)].str()
};
}
最后我们返回一个字符串数对,first为顶点着色器代码,second为片段着色器代码。
ShaderLoader中的其他函数与ModelShader大同小异,此处就不再详细说明。
同时,loader中还有一个工具——textureloader,我们将会与texture类一起在以后讲解。
|