您现在的位置是:首页 >技术杂谈 >实现图形算法API[软光栅渲染器,C++]网站首页技术杂谈

实现图形算法API[软光栅渲染器,C++]

刘欢明 2024-06-17 11:26:54
简介实现图形算法API[软光栅渲染器,C++]

最近有点烦,发烧感冒了三天[事实上是俩天,第三天是因为摆得太舒服了索性多玩一天],啥都没学,打守望先锋也把把被虐...,想着今天来提起键盘把之前的东西都总结一下。

那么话归真题,首先我是仿造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...怎么能抹除数据的差别,用一种方法插值呢?

等等等等等都是问题,但好在我解决了;

下面奉上效果图:

 

 

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。