最近有点烦,发烧感冒了三天[事实上是俩天,第三天是因为摆得太舒服了索性多玩一天],啥都没学,打守望先锋也把把被虐...,想着今天来提起键盘把之前的东西都总结一下。
那么话归真题,首先我是仿造opengl来写的图形api,因为是在cpu端运行,所以要比opengl优雅很多。
首先肯定要预先计划好写哪些功能,我想的第一个是vertexbuffer,因为这个是最经常接触的,而且实现思路也非常清晰明了。
class VertexBuffer
{
struct DataWarp {
ITypeTag* info = nullptr;
size_t InfoSize = 0;
void* data = nullptr;
size_t dataElemSize = 0;
~DataWarp() {
memory::dealloc(data, info->tSize);
memory::dealloc(info, InfoSize);
}
};
public:
/*
* T 数据的类型
* bufferindex 所在的缓冲下标
* dataIndex 缓冲中对应的数据下标
*/
template<class T>
T* Sample(size_t bufferIndex, size_t dataIndex);
/*
* index 布局的下标
* size 数据的字节数
* data 数据指针
* T 数据的类型
*/
template<class T>
void SetData(size_t index, size_t size, void* data);
private:
std::vector<DataWarp*> buffers;
friend class IInvoker;
};
这个是我设计的类;
只暴露了两个方法,一个push数据,一个采样数据,push数据的方法模仿了opengl,给定对应的布局下标以及数据内存的大小和数据指针,然后将数据拷贝;
当然,我这边有一个问题,buffers里面的指针应该用智能指针代替;
SetData的方法:
template<class T>
inline void VertexBuffer::SetData(size_t index, size_t size, void* data)
{
if (!data) return;
if (index >= buffers.size())
buffers.resize(index + 1);
buffers[index] = new DataWarp;
buffers[index]->data = memory::alloc<void>(size);
memcpy(buffers[index]->data, data, size);
buffers[index]->info = new TypeTag<T>();//注意memory::alloc不会调用构造函数
buffers[index]->InfoSize = sizeof(TypeTag<T>);
buffers[index]->dataElemSize = size / sizeof(T);
}
这边要提及一下TypeTag这个类了,这个是本人的一个小发明,主要是将数据类型抹除,然后用数据去封装,以达到运行时数据类型检测的效果,这个在整个框架设计中都非常重要;
这边我们将采样步长[T的字节数]等信息都封存到TypeTag中,这些信息在我们采样数据的时候是必须要有的;
这样我们就有了最基础的push数据和采样数据的功能了;
另外一个类的设计是uniformbuffer
class UniformBuffer
{
struct DataWarp {
ITypeTag* info;
size_t InfoSize;
void* data;
~DataWarp() {
memory::dealloc(data, info->tSize);
memory::dealloc(info, InfoSize);
}
};
public:
template<class T>
void PushBufferData(std::string name,void* data);
template<class T>
T* GetBufferData(std::string name);
template<class T>
void SetBufferData(const std::string& name,const T& data);
void SetCallBack(std::function<void(const std::string&, void*)>);
private:
std::unordered_map<std::string, DataWarp*> buffers;
std::function<void(const std::string&, void*)> callback;
friend class IInvoker;
};
我们这边和opengl一样,是通过变量名表意字符串进行索引的,大概思路和vertexbuffer差不多,一个push数据的方法,一个采样数据的方法,其外有两个不同的方法,一个是修改数据的方法,因为vertexbuffer中的数据一般都不会被修改,所以就没提供这个方法,但是uniform不一样,里面的数据是会被经常修改的所以提供了一个修改数据的方法;
uniformbuffer中的数据会被渲染器分配到shader中,所以uniformbuffer中的数据修改时,shader中的数据也必须被修改,这个时候有两个方向两个思路的解决方向;
先说思路,一个是主动思路,一个是被动思路,其中主动思路的意思是,每帧的渲染中渲染器都将uniformbuffer中的数据重新分配给shader,还有一种思路是被动思路,意思是只有当uniformbuffer中的数据被修改的时候才会去重新分配,这样的好处是降低性能开销,我选择了后者;
所以我增加了一个setCallback的方法,当数据被修改的时候会回call,以达到重新分配的目的;
另外一个类是indexbuffer这个buffer就没什么说头了和vertexbuffer差不多,只不过相比上两个buffer,indexbuffer的数据是提供给渲染器的,而那两个是提供给shader的;
另外一个类的话,是invoker;
class IInvoker
{
protected:
std::shared_ptr<UniformBuffer> uniformBuffer;// shader have uniformBuffer;
std::shared_ptr<VertexBuffer> vertexBuffer;
std::shared_ptr<IndexBuffer> indexBuffer;
std::shared_ptr<IShader> shader;
std::shared_ptr<ITexture> zBuffer;
OutBufferManger* outBufferManger;
Interpolator* intpor;//intpor是由vertexShader收集信息创建的,但由IInvoke持有,其不是一个指针对象,所以不用shared管理
glm::mat4 viewport;
glm::vec2 sampleSpace;
glm::vec2 zbufferSS;
std::function<bool(const float&, float&)> depthTest;
void CheckUniformBuffer();
void CheckVertexBufferLayout();
void CheckIntpor();
void UniformCallback(const std::string&, void*);
//这个是vertexbuffer与vertexShader数据连接的桥梁,将数据按照layout进行匹配
void SetLayoutDataForVertexShader(size_t);
void SetUniformForShader();
void fill_triangle_optimized(glm::ivec2 v1, glm::ivec2 v2, glm::ivec2 v3, const glm::vec3* vn, const glm::vec4* vPos);
void SettleFragmentShaderRes(float x, float y);
void TreatPixel(const int& x, const int& y,const glm::vec3* vn,const glm::vec4* vPos);
glm::vec3 barycentric(glm::vec2 v1, glm::vec2 v2, glm::vec2 v3, glm::vec2 p);
void Interplator(glm::vec3 Bar);
public:
enum class Status {
COMPELETED,FAILED
};
void Link();
virtual void lnvokeForTriangle();
Status vertexBufferStatus = Status::FAILED;
Status uniformBufferStatus = Status::FAILED;
Status InterpolatorStatus = Status::FAILED;
friend int main();
friend struct LiRenderer;
};
这个invoker是一个算法模板,我是为了将算法模板和渲染器解耦才设计这个类的,这个类中包括但不限于三角形填充算法、线性插值、重心插值、深度测试等等算法,而且为了可装配性,我将大部分算法模块都抽象成了一个std::function<>的接口,这样就可以在渲染器具中开放接口,让客户端程序员进行装配;
另外这个类我有一个做的不够好的地方,那就是校验数据的工作我给分配到其中了,但它只是一个算法模板,后面我们给修改的;
另外还有一个类,OutBufferManger,这个类的话的作用就相当于OpenGL中的多渲染目标了,一次渲染可以渲染出多张纹理;
class OutBufferManger
{
struct TexAndNVar {
void* var;//链接到framebuffer中的值的指针
std::shared_ptr<ITexture> tex;
};
public:
OutBufferManger() {}
void PushTexure(size_t index, std::shared_ptr<ITexture> texture);
void SetData(size_t index, float x, float y, void* data);
void SetData(size_t index, size_t x, size_t y, void* data);
void TakeData(size_t index, float x, float y);
void TakeData(size_t index, size_t x, size_t y);
std::shared_ptr<ITexture>& GetTex(size_t index) { return ts.at(index).tex; }
private:
std::vector<TexAndNVar> ts;
friend struct LiRenderer;
friend struct IInvoker;
};
这个类最终是要与fragmentshader进行对接的,将fragmentshader中数据与其中的Texture类进行绑定,渲染出的数据push到其中;
至于shader类就比较复杂了,因为涉及到很多种数据的准备工作,比如vertexshader要准备布局数据和传递到fragmentshader中的数据还有uniform数据,fragment也要准备很多数据;
这些数据的准备都是在构造函数中完成的,在基类中会完成必要的数据准备,子类中就是各种客户端程序员的自定义数据准备了;
另外还有很多有意思的问题,比如插值,怎么对一个三角形进行插值?用重心坐标,但是可能被插值的数据类型有那么多种,int、float、vec2、vec3...怎么能抹除数据的差别,用一种方法插值呢?
等等等等等都是问题,但好在我解决了;
下面奉上效果图: