一个小型的3D渲染器,主要是在学习OpenGL时候做的实践玩具🤣,可以内嵌于任何支持OpenGL的环境中。
- 模型加载和渲染
- 三种光照:平行光,点光源,聚光
- 阴影(目前只有平行光有)
- 天空盒子
- 纹理加载和渲染,支持漫反射纹理,法线纹理,镜面光纹理和放射光纹理
- 允许手动配置纹理和材质的多边形几何体
在开始使用前你可能想要运行一下看看效果。在test
文件夹下是测试文件,里面包含了渲染器各个功能的测试。
需要用到的第三方库:
- GLEW:用于引导OpenGL
- SDL:用于提供窗口平台
- Assimp:用于加载模型
- glm:用于数学运算
在根目录
下使用make
编译:
make
然后直接在根目录运行测试:
./test/TestClearScreen
所有的文件在include
文件夹下,源代码在src
下,别忘记将shader
文件夹复制到你的工程以便于渲染器找到着色器。
首先你必须配置好OpenGL的环境(包括窗口环境和调用glewInit()
函数),然后使用CreateRenderer()
函数创建渲染器:
tinyrenderer3d::Renderer* render = CreateRenderer(WindowWidth, WindowHeight);
不要忘记在程序退出时删除:
tinyrenderer3d::DestroyRenderer(render);
由于这只是个渲染器而不是个引擎,所以他不会帮你自动删除任何你使用CreateXXX
函数创建出来的东西,你必须使用相对应的DestroyXXX()
函数来释放你创建出来的东西。
然后你可以使用render->SetClearColor()
和render->Clear()
来进行清屏。
Tinyrenderer3D允许你给入顶点,材质来自己配置一个几何体,这里以绘制平面举例(详情可见test/TestDrawMesh.cpp)
首先所有的几何体(包括模型导入的几何体)都是Mesh
类型,通过实例化Mesh类和配置其中的信息,我们可以自定义一个几何体:
const vector<Vertex> vertices = { // 几何体的顶点属性
{{ 0.5f, 0.0f, 0.5f}, {1.0f, 0.0f}, {0.0f, 1.0f, 0.0f}},
{{ 0.5f, 0.0f, -0.5f}, {1.0f, 1.0f}, {0.0f, 1.0f, 0.0f}},
{{-0.5f, 0.0f, -0.5f}, {0.0f, 1.0f}, {0.0f, 1.0f, 0.0f}},
{{-0.5f, 0.0f, 0.5f}, {0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}},
};
const vector<uint> indices = { // 几何体顶点的一个索引
0, 1, 2,
0, 2, 3
};
plane_.center = {0, -1, 0}; // 几何体的中心坐标
plane_.vertices = vertices; // 将顶点传入
plane_.indices = indices; // 将索引传入
plane_.scale = {10, 1, 10}; // 设置缩放
// 接下来设置材质
plane_.material.ambient = {0.6, 0.6, 0.6};
plane_.material.specular = {0.6, 0.6, 0.6};
plane_.material.shininess = 32;
// 如果有纹理的话设置纹理,就不需要设置相关材质了
plane_.material.diffuse_texture = texture_;
vertices
是几何体的顶点,Vertex
类型一共有五个属性:
position
:顶点的位置texcoord
:如果有纹理的话,纹理坐标normal
:顶点的法向量tangent
:如果有法线贴图的话,法线贴图的切向量bitangent
:如果偶发现贴图的话,法线贴图的副切向量
如果你不需要其中的某些信息可以不设置,但是一定要设置position
,texcoord
,normal
。
indices
指定了顶点索引,如果你没有索引也可以不给(让plane_.indices
置空即可)。
然后是几何体的设置和材质的设置。
材质方面除了可以直接给出颜色之外(颜色在0到1之间),还允许有纹理。存在
- 漫反射纹理
material.diffuse_texture
- 法线纹理
material.normal_texture
- 镜面光纹理
material.specular_texture
- 放射光纹理
material.emission_texture
如果不需要设置为nullptr
即可。设置了贴图就不需要设置相关的颜色了。
当你设置好所有的信息后,通过render->AddObject(&plane_)
将其放入渲染器中渲染:
render->AddObject(&plane_);
别忘记使用Draw()
函数将物体绘制到屏幕上:
render->Draw();
这是测试用例的效果,展示了有贴图和没贴图的两种情况:
默认场景中存在一个平行光,其会以(1, 1, 1)
的全局光分量,向着(0, -1, -1)
的方向照射(就是最亮的全局光),这会导致场景中的所有物体都被照到最亮。
如果你想要自己设置光线,请通过实例化LightSet
类并且使用render->SetLights()
来设置,像这样(参考test/TestLight.cpp):
LightSet lights;
// 设置平行光,这里将平行光灯关掉以突显点光源和聚光的效果
lights.dirlight.SetAmbient(0, 0, 0);
lights.dirlight.SetDirection(0, 0, 0);
lights.dirlight.SetSpecular(0, 0, 0);
// 设置点光源
DotLight dotlight;
dotlight.SetPosition(1, 1, 1);
dotlight.SetAmbient(0, 0, 0);
dotlight.SetDiffuse(0.55, 0.55, 0.55);
dotlight.SetSpecular(0.55, 0.55, 0.55);
lights.dotlights.push_back(dotlight);
// 设置聚光
SpotLight spotlight;
spotlight.SetPosition(0, 1, 0);
spotlight.SetDirection(0, -1, 0);
spotlight.SetAmbient(0.8, 0.8, 0.8);
spotlight.SetDiffuse(0.8, 0.8, 0.8);
spotlight.SetSpecular(0.4, 0.4, 0.4);
spotlight.SetParameter(50, 40); // 设置聚光的外切角和内切角
lights.spotlights.push_back(spotlight);
// 设置新的光照
render->SetLights(lights_);
这里给出测试用例的效果:
场景上方一个聚光向下照射,(1, 1, 1)
处有一个点光源(可以看到地板的远处和近处的亮度不一样,这是点光源导致的)。
绿色的那个是放射光贴图,可以自己发光,不受光照影响。
通过实例化Model
类型,并通过LoadModel()
就可以了。别忘记通过render->AddObject()
加入到render中。
增加天空盒的方法很简单,先通过CubeMap::CubeMapTextureDatas
配置到天空盒的六个面,然后通过CreateCubeMap()
得到一个CubeMap,然后通过CreateSkyBox()
得到一个天空盒子即可(参考test/TestSkybox.cpp):
// 载入图像
CubeMap::CubeMapTextureDatas datas;
datas.negative_x = LoadImage("test_resources/skybox/left.jpg");
datas.negative_y = LoadImage("test_resources/skybox/bottom.jpg");
datas.negative_z = LoadImage("test_resources/skybox/back.jpg");
datas.positive_x = LoadImage("test_resources/skybox/right.jpg");
datas.positive_y = LoadImage("test_resources/skybox/top.jpg");
datas.positive_z = LoadImage("test_resources/skybox/front.jpg");
CubeMap* cubmap = CreateCubeMap(datas);
SkyBox* skybox = CreateSkyBox(cubmap);
// 创建Cubemap后,其会自动将图像放到显存,所以这里可以直接删了
DestroyImage(datas.negative_x);
DestroyImage(datas.negative_y);
DestroyImage(datas.negative_z);
DestroyImage(datas.positive_x);
DestroyImage(datas.positive_y);
DestroyImage(datas.positive_z);
// 设置天空盒子
render->SetSkybox(skybox);
你也可以通过render->RemoveSkyBox()
移除当前的天空盒子。
这里是测试用例的效果:
render自带一个可以水平移动的摄像机。通过render->GetCamera()
得到他,并且在你的事件处理中对其视角进行处理(详情见test/test_framework.hpp中的MainLoop
函数)。
法线贴图:
模型加载: