/TinyRenderer3D

a small 3D renderer based on OpenGL

Primary LanguageC

TinyRenderer3D

一个小型的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();

这是测试用例的效果,展示了有贴图和没贴图的两种情况:

drawMesh

增加一些光源

默认场景中存在一个平行光,其会以(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_);

这里给出测试用例的效果:

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()移除当前的天空盒子。

这里是测试用例的效果:

skybox

摄像机

render自带一个可以水平移动的摄像机。通过render->GetCamera()得到他,并且在你的事件处理中对其视角进行处理(详情见test/test_framework.hpp中的MainLoop函数)。

其他的效果展示

法线贴图:

normal map

模型加载:

model