好的,我们继续进行场景结构的编写。
如同上一篇预告的,首先让我们来编写存储材质的数据结构。首先上代码∶
class SceneObjectMaterial : public BaseSceneObject { protected: Color m_BaseColor; Parameter m_Metallic; Parameter m_Roughness; Normal m_Normal; Parameter m_Specular; Parameter m_AmbientOcclusion; public: SceneObjectMaterial() : BaseSceneObject(SceneObjectType::kSceneObjectTypeMaterial) {}; };
材质的模型有很多种,在游戏当中常用的是经典的高光漫反射模型,还有近年流行的基于物理的PBR模型。上面这个例子就是PBR模型。
计算机图形学当中严格意义上的材质模型是指物体表面对于来自各个方向的光线的反射折射以及吸收的数学模型。多为解析形式或者隐函数模式。游戏当中使用的材质的含义则更为宽泛,而且一般为离散采样样本数据和合成公式的模式,有的时候也被称为是基于图像的渲染方式(Image Based Rendering)
这是因为游戏作为一种交互式数字艺术作品,并不严格追求所渲染的图像的正确性。而基于解析计算的渲染方法目前计算量都很大,而且包含很多逻辑判断与分支,难以实现GPU的批量化计算。而基于图像的渲染则是将事先准备好的图片(贴图)按照一定的规律进行组合输出结果,这种方式特别适合并行计算,而且结果“看起来”也能够相当地不错。
使用过近代DCC工具的人应该都知道,DCC工具当中的材质,除了极少的一部分之外,大多也是基于图像的。这种材质的特点是,可以为材质的每个属性(或者说通道)指定一个固定的值,或者是一张贴图。通过巧妙地组合这些贴图和通道,可以形成各种各样十分有趣或者十分逼真的材质效果,比如生锈、浸润、污渍、蚀刻、风化、毛绒、伤疤等等。所以我们的材质属性也需要支持单值输入的情况,和贴图输入的情况。这是通过定义如下的复合数据类型来实现的∶
template <typename T> struct ParameterMap { bool bUsingSingleValue = true; union _ParameterMap { T Value; std::shared_ptr<Image> Map; }; }; typedef ParameterMap<Vector4f> Color; typedef ParameterMap<Vector3f> Normal; typedef ParameterMap<float> Parameter;
好,接下来让我们看看灯光。游戏当中常用的灯光有泛光灯(也叫点状光源,白炽灯)、射灯、天光等。泛光灯是向其周边360度球形空间均匀辐射光线的灯,也就是无指向的灯;射灯是向特定方向进行一个光锥照射的灯,比如舞台上打的那种追光灯,或者汽车的大灯;天光则是平行光源。
无论是哪种灯,首先都有个自身的亮度或者说光线密度属性,然后会有一个光线衰减函数。虽然在物理学当中,光线是按照距离的平方倍进行衰减的,但是在游戏制作当中这样的衰减很不容易控制,而且往往衰减得过快。所以,如同我们在很多DCC工具当中看到的,我们往往是通过指定一个近裁剪平面,一个远裁剪平面和一个近似的光衰减函数来控制光照的效果。所以我们的光照基类的定义如下∶
class SceneObjectLight : public BaseSceneObject { protected: Color m_LightColor; float m_Intensity; AttenFunc m_LightAttenuation; float m_fNearClipDistance; float m_fFarClipDistance; bool m_bCastShadows; protected: // can only be used as base class of delivered lighting objects SceneObjectLight() : BaseSceneObject(SceneObjectType::kSceneObj ectTypeLight) {}; };
这里面上面没有提到的就是m_bCastShadows。这是用来标识光源是否会产生阴影的。阴影的形成因为要做以光源为视点的场景投影与排序,对于实时渲染来说是很昂贵的。
我们如果用过近代的一些游戏引擎,我们会看到除了上面这些属性之外,还会有诸如光源是否会移动等诸多其它属性。这些属性更多的是为了优化光照计算性能存在的,我们在今后相应的地方再进行拓展,这里先不介绍了。
接下来我们就可以从这个基类进行派生,定义各种具体的光源类型了∶
class SceneObjectOmniLight : public SceneObjectLight { public: using SceneObjectLight::SceneObjectLight; }; class SceneObjectSpotLight : public SceneObjectLight { protected: float m_fConeAngle; float m_fPenumbraAngle; public: using SceneObjectLight::SceneObjectLight; };
可以看到泛光灯最简单,基本就是基类本身;射灯则多了两个光锥顶角相关的参数。
这里可能有人会问,射灯的方向的定义是不是被遗漏了?回答∶是的,但是是故意的。我们的之前的设计是,场景对象当中只保存场景对象固有的属性,而与场景结构或者空间位置相关的参数是保存在场景节点而不是场景对象当中的。在游戏当中往往会采用追光灯的设计,也就是跟着某个对象跑的射灯。但是被跟随的对象的空间位置只有场景节点和场景图这一级的信息当中才有,将这个信息写到场景对象里面是不合适的。
关于光照函数我们则是如下定义的∶
typedef float (*AttenFunc)(float /* Intensity */, float /* Distance */);
就是一个输入为光源的强度和到光源的距离,输出为光照强度的函数的指针。
最后是摄像机的部分了。摄像机和射灯某种意义类似,位置和方向我们放在节点当中处理,作为场景对象主要需要记录的是纵横比,近裁剪距离和远裁剪距离。对于透视相机还需要记录fov,即视角。
class SceneObjectCamera : public BaseSceneObject { protected: float m_fAspect; float m_fNearClipDistance; float m_fFarClipDistance; public: SceneObjectCamera() : BaseSceneObject(SceneObjectType::kSceneObjectTypeCamera) {}; }; class SceneObjectOrthogonalCamera : public SceneObjectCamera { }; class SceneObjectPerspectiveCamera : public SceneObjectCamera { protected: float m_fFov; };
正交相机是指没有透视到相机,一般用于在3D空间当中制作2D游戏。而透视相机的fov则是表示了相机的视场角,大的视场角相当于广角相机,小的则相当于长焦。当然,仅仅是这样的话是没有景深效果的。景深效果是由于凸透镜成像造成的,而透视相机只是相当于在正交相机上多乘一个仿射矩阵(透视矩阵)P,每个像素依然是单根光线采样决定其颜色值,是和理想的小孔成像一样的。
好了,到这里场景对象的主要结构就完成了。对比前面的设计,我们还有诸如变形对象,贴图对象,动画对象,矩阵对象等没有完成。不过这些对于渲染一个基本的场景来说是不需要或者可以用别的方法代替的,就不在这里一一表述了。下面我们进入场景结构(场景图),并且对接OpenGEX,导入一个实际的场景并进行画面的渲染。