首页 > Personal > Game > 游戏引擎设计系列9-渲染系统
2018
09-27

游戏引擎设计系列9-渲染系统

占位
Texture、Mesh、StaticMesh、SkinedMesh、Material、Shader、Resource

Resource
资源系统,用来管理资源的加载,保存和生成,同时处理依赖关系,在引擎中大部分资源都是Lua格式,通过读取加载.
需要以资源方式管理的资源继承自Resource,并处理相关函数.

法线贴图
一般法线贴图使用tangent空间坐标存储,使用UV方向作为Tangent(X轴)和Binormal(Y轴)方向,normal=TangentXBinormal(Z轴)

因为一个向量每个维度的取值范围在(-1, 1),而纹理每个通道的值范围在(0, 1),因此我们需要做一个映射,即pixel = (normal + 1) / 2。这样,法线值(0, 0, 1)实际上对应了法线纹理中RGB的值为(0.5, 0.5, 1),而这个颜色也就是法线纹理中那大片的蓝色。这些蓝色实际上说明顶点的大部分法线是和模型本身法线一样的。
Fragment的光照信息也可以转为tangent坐标进行光照计算。
使用tangent空间存储normal,因为是相对的法线信息,应用到任何模型上都能得到合理的光照,如果使用模型坐标系就不可以了。
可以使用UV动画。我们可以移动一个纹理的UV坐标来实现一个凹凸移动的效果,如水或者火山熔岩这种类型的物体。

渲染的代码层面,
BeginScene 开始渲染
SetRenderTarget 设置渲染的颜色surface,
SetDepthStencilSurface 设置渲染的Depth或StencilSurface
Clear 清空前面设置的Color Buffer、Depth Buffer或者Stencil Buffer
SetTexture 按贴图寄存器设置贴图DiffuseMap、LightMap等。
SetRanderState 设置渲染需要的相关状态,如AlphaBlend、AlphaTest、ZWrite、CULLMODE(正反面剔除)
SetVertexShaderConstantF 将Vertex需要的参数按寄存器地址传递到Shader,如UV、骨骼的权重、骨骼实时信息、World Matrix、View Matrix、Projection Matrix、View Projection Matrix等等。为Uniform
SetPixelShaderConstantF 将Fragment需要的参数按寄存器地址传递到Shader,如光照信息、View Pos信息等等。为Uniform
SetPixelShader 设置Pixel Shader
SetVertexShader 设置Vertex Shader
DrawPrimitiveUp 直接渲染Vertex序列
DrawIndexedPrimitive 渲染IndexBuffer和VertexBuffer中的内容。提前准备IndexBuffer(并用SetIndices设置)、VertexBuffer(并用SetStreamSource设置),并用SetVertexDeclaration设置Vertex描述(在之前通过CreateVertexDeclaration创建不同类型的Vertex的描述,如Static、Skin等)
EndScene 结束渲染
Present 交换Back Buffer到Front Buffer,Front Buffer的内容会显示在屏幕上

VS语义 POSITION BLENDWEIGHT NORMAL TANGENT BINORMAL PSIZE BLENDINDICES TEXCOORD0-TEXCOORD7
PS语义 POSITION PSIZE FOG COLOR0-COLOR1 TEXCOORD0-TEXCOORD7
VS输出中必须包含POSITION语,该值不能在PS中直接使用,它只被用于光栅化。VS输出中的自定义数据可以使用TEXCOORD系列来表示。
PS输入除POSITION外,VS输出语义,也是PS的输入语义 PS输出语义通常只有一个输出COLOR,最终颜色值。
代码中通过CreateVertexDeclaration创建Vertex描述,如下
static const D3DVERTEXELEMENT9 ve[] = {
{ 0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0 }, //position 12
{ 0, 12, D3DDECLTYPE_FLOAT16_4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 0 }, //normal 8
{ 0, 20, D3DDECLTYPE_FLOAT16_4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TANGENT, 0 }, //tangent 8
{ 0, 28, D3DDECLTYPE_FLOAT16_4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_BINORMAL, 0 }, //binormal 8
{ 0, 36, D3DDECLTYPE_UBYTE4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_BLENDINDICES, 0 }, //blendindices 4
{ 0, 40, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_BLENDWEIGHT, 0 }, //blendweight 16
{ 0, 56, D3DDECLTYPE_FLOAT16_2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0 }, //uv0 4
{ 0, 60, D3DDECLTYPE_FLOAT16_2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 1 }, //uv1 4
D3DDECL_END()
};
CreateVertexDeclaration(ve, &VD);
在渲染之前通过SetVertexDeclaration(VD)设置,并根据上面描述的相应内容准备VertexBuffer和IndexBuffer。

渲染管线(Render PipeLine)
模型按不同材质整理了相应点的序列 -> 传入GPU -> Vertex Shader 处理顶点信息 -> 光栅化(插值)为屏幕上的fragment -> Framgent Shader处理像素光照颜色等信息 -> back buffer -> 后期处理 -> front buffer -> 屏幕
顶点处理阶段 物体空间->世界空间->观察空间(透视Perspective(物体近大远小)、正交Orthographic(物体大小恒定))->裁剪空间(近截面、远截面)
fragment 处理阶段 裁剪空间->屏幕空间

顶点光照渲染(Vertex Render):在Vertex阶段,所有光源的照明都是在物体的顶点上进行计算,无法支持逐像素渲染效果,如阴影、法线贴图、灯光遮罩、高光等

正向渲染(Forward Render):在Vertex阶段和Fragment阶段,对每个顶点或每个像素进行光照计算,对每个光源进行计算产生最终结果。光源数量对计算复杂度影响巨大,比较适合户外光源较少的场景。
优化方案,
一般屏幕像素较多,如果顶点数小于像素个数的话,尽量在vertex shader中进行光照。
vertex 中的光照可以优化同时处理,如Unity同时处理4个光源
静态物体可以使用预生成LightMap,减少光照处理
根据点光源的作用区域,不作用区域的像素就不进行处理
球谐光照(Spherical Harmonic Lighting),根据光照方程在cpu端为每个顶点计算球谐系数,传入GPU(Vertex 阶段)利用球谐系数还原光照方程计算光照,复杂度和光源个数不相关,使用LightMap的物体一般不计算球谐光照,而且球谐光照一般只影响Diffuse,主要用来计算动态物体
一般不重要的光源以逐顶点或球谐的方式进行计算,最亮的方向光源为像素光源,重要的光源为像素光源
不同的光照可以有重叠,如最后一个逐像素光源也以逐顶点光照模式的方式渲染,这样能减少当物体和灯光移动时可能出现的”光照跳跃”现象

延时渲染(Defered Rendering)
将光源的数目和场景中物体的数目在复杂度层面上完全分开,使用G-Buffer用来存储每个像素对应的Position,Normal,Diffuse Color和Material parameters,常见的做法是将颜色,深度和法线分别渲染到不同的buffer里面,在最后计算光照的时候的通过这三个buffer和光源的信息计算出最终pixel的颜色。
没有光源数量限制,光源数量只和产生的阴影数量相关。
只能支持有限的光照模式如Blinn-Phong,所有光照以同样的方式计算
不好实现抗锯齿

延时光照(Defered Lighting)
类似于延时渲染,但是G-Buffer不再存储材质信息,只存储光照信息。对不同材质的物体使用不同的shader进行渲染,每个物体的材质参数将有更多变化。同时减少了G-Buffer的存取带宽
Base Pass,渲染物体,生成depth, normals, specular power,存入G-Buffer
Lighting pass,使用上一步的缓冲计算出光照。光照渲染基于深度,法线和高光强度计算光照,光照是被屏幕空间被计算。存入Light-Buffer
Final pass,再次渲染物体,用已经计算好的光线和颜色纹理混合在一起,然后再加上环境光以及散射光照。以forward rendering渲染
相对于延时渲染,由于使用了Z值和Normal值,就可以很容易找到边缘,并进行采样,来使用使用MSAA。

分块的延时光照(Tile-Based Deferred Rendering)
就是将屏幕分成一个个小块tile。然后根据这些Depth求得每个tile的bounding box。对每个tile的bounding box和light进行求交,这样就得到了对该tile有作用的light的序列。最后根据得到的序列计算所在tile的光照效果。
对比Deferred Render,之前是对每个光源求取其作用区域light volume,然后决定其作用的的pixel,也就是说每个光源要求取一次。而现在,只要遍历每个pixel,让其所属tile与光线求交,来计算作用其上的light,并利用G-Buffer进行Shading。一方面这样做减少了所需考虑的光源个数,另一方面与传统的Deferred Rendering相比,减少了存取的带宽。

Alpha Test, 根据alpha是否满足条件来决定是否忽略Fragment,同时可以进行正常的深度检查,所以不需要关闭ZWrite。产生的效果要么完全透明,要么完全不透明。
Stancil Buffer, 为屏幕上的每个像素保存一个无符号整数值(256)。
Stancil Test, 在渲染的过程中,用Stancil Buffer与一个参考值相比较,根据比较的结果来决定是否更新相应的像素点。发生在alpha test之后,depth test之前。
Depth Buffer,就像颜色缓冲(Color Buffer)那样存储每个Fragment的信息,深度缓冲由窗口系统自动创建并将其深度值存储为 16、 24 或 32 位浮点数。在大多数系统中深度缓冲区为 24 位。
Depth Test,检测Depth Buffer内的深度值。如果测试通过,深度缓冲内的值将被设为新的深度值。如果深度测试失败,则丢弃该Fragment。
现在大多数 GPU 都支持一种称为提前深度测试(Early depth testing)的硬件功能。提前深度测试允许深度测试在片段着色器之前运行。明确一个片段永远不会可见的(它是其它物体的后面) 我们可以更早地放弃该片段。
Alpha Blending, 使用当前fragment的alpha作为混合因子,来混合之前写入到缓存中颜色值。麻烦的一点就是它需要关闭ZWrite,并且要十分小心物体的渲染顺序。如果不关闭 ZWrite,那么在进行深度检测的时候,它背后的物体本来是可以透过它被我们看到的,但由于深度检测时大于它的深度就被剔除了,从而我们就看不到它后面的物体了。因此,我们需要保证物体的渲染顺序是从后往前,并且关闭该半透明对象的ZWrite。
Fog(雾),是根据绘制像素的深度值,调整预先设定的雾的混合颜色来进行处理。越远的地方的像素颜色越接近白色,用作表现场景深处可以看到朦胧感的空气。Fog,深度值越远就有越模糊的像素值的空气远近表现法。

半透明物体渲染
在shader中,我们可以设置渲染类型,设置渲染队列值,渲染队列值表示该物体的绘制顺序。
先渲染不透明物体。
在引擎中对半透明物体按照里View的远近排序,关闭ZWrite、开启Alpha Blend,按由远到近的顺序依次渲染。
对于相互交叉的半透明物体或这半透明物体的前后自遮挡,如果要渲染正确相对比较麻烦,Nvidia的Demo里有实例,好像需要3个pass。

阴影
以光源方向的正交投影渲染所有需要产生阴影的物体,生成Shadow Map,
再渲染接受阴影的物体深度,VS传出一个世界坐标,并传入光源空间变换矩阵,在PS中将这个世界坐标转为光源空间下的坐标,(x,y)就是Shadow Map中的对应位置,计算出对应光源空间的深度值(在正交空间中算深度就是P.z / P.w,正交空间中的p.w是固定值)。比较这个深度值与Shadow Map中对应位置的值的大小,就可以确定是否是阴影了。

Skined Mesh
双四元数算法,在CPU根据骨骼计算骨骼的双四元数
Array model_pose = pose->GetModelPose(); 获取当前Pose的ModelTransfrom
Array skeleton_pose = skeleton->GetModelPose(); 获取skeleton的ModelTransfrom
遍历骨骼节点计算双四元数bone[2]
Transform model_transform = model_pose[joint_id];
Transform skeleton_transform = skeleton_pose[joint_id];
Quaternion quat = skeleton_transform.rotation.Conjugate() * model_transform.rotation;
Vector3 translation = (-skeleton_transform.position) * quat + model_transform.position;
bone[0].x = quat.x;
bone[0].y = quat.y;
bone[0].z = quat.z;
bone[0].w = quat.w;
bone[1].x = 0.5f * ( translation[0] * quat.w + translation[1] * quat.z – translation[2] * quat.y);
bone[1].y = 0.5f * (-translation[0] * quat.z + translation[1] * quat.w + translation[2] * quat.x);
bone[1].z = 0.5f * ( translation[0] * quat.y – translation[1] * quat.x + translation[2] * quat.w);
bone[1].w =-0.5f * ( translation[0] * quat.x + translation[1] * quat.y + translation[2] * quat.z);
将计算结果传入Vertex Shader,具体Shader变换代码如下,bone(上面计算的双四元数)通过SetVertexShaderConstantF传入
struct Input {
half3 Position : POSITION;
half3 Normal : NORMAL;
half3 Tangent : TANGENT;
half3 Binormal : BINORMAL;
uint4 BoneIndex : BLENDINDICES;
half4 BoneWeight : BLENDWEIGHT;
half2 UV0 : TEXCOORD0;
half2 UV1 : TEXCOORD1;
};

struct Output {
half4 Pos : POSITION;
half4 Position : TEXCOORD0;
half3 Normal : TEXCOORD1;
half3 Tangent : TEXCOORD2;
half3 Binormal : TEXCOORD3;
half2 UV0 : TEXCOORD4;
half2 UV1 : TEXCOORD5;
half4 ViewVec : TEXCOORD6;
half4 WorldPos : TEXCOORD7;
};

Output vs(Input i) {
Output o;
half2x4 m = bone[i.BoneIndex.x];
half4 dq0 = (half1x4)m;
half2x4 dual = i.BoneWeight.x * m ;
m = bone[i.BoneIndex.y];
half4 dq = (half1x4)m;
dual += i.BoneWeight.y * m * normalize(dot(dq0, dq));
m = bone[i.BoneIndex.z] ;
dq = (half1x4)m ;
dual += i.BoneWeight.z * m * normalize(dot(dq0, dq));
m = bone[i.BoneIndex.w];
dq = (half1x4)m;
dual += i.BoneWeight.w * m * normalize(dot(dq0, dq));
half length = sqrt(dual[0].w * dual[0].w + dual[0].x * dual[0].x + dual[0].y * dual[0].y + dual[0].z * dual[0].z) ;
dual[0] = dual[0] / length ;
dual[1] = dual[1] / length ;
half3 p = i.Position.xyz + 2.0 * cross(dual[0].xyz, cross(dual[0].xyz, i.Position.xyz) + dual[0].w * i.Position.xyz) ;
p = p + 2.0 * (dual[0].w * dual[1].xyz – dual[1].w * dual[0].xyz + cross(dual[0].xyz, dual[1].xyz));
o.Pos = half4(p,1);
o.UV0 = i.UV0;
o.UV1 = i.UV1;

o.Normal = i.Normal.xyz + 2.0 * cross(dual[0].xyz, cross(dual[0].xyz, i.Normal.xyz) + dual[0].w * i.Normal.xyz);
o.Tangent = i.Tangent.xyz + 2.0 * cross(dual[0].xyz, cross(dual[0].xyz, i.Tangent.xyz) + dual[0].w * i.Tangent.xyz);
o.Binormal = i.Binormal.xyz + 2.0 * cross(dual[0].xyz, cross(dual[0].xyz, i.Binormal.xyz) + dual[0].w * i.Binormal.xyz);

o.Pos = mul(World, o.Pos);
o.WorldPos = o.Pos;
half3 vec = ViewPos – o.WorldPos;
o.ViewVec.xyz = normalize(vec);
o.ViewVec.w = dot(o.ViewVec.xyz, vec);
o.Normal = mul(World, o.Normal);
o.Tangent = mul(World, o.Tangent);
o.Binormal = mul(World, o.Binormal);
o.Pos = mul(ViewProjection, o.Pos) * LeftHand;
o.Position = o.Pos;
}

Decal
算出需要贴Decal物体的射线交点并获取normal,按normal方向把Decal渲染出来,为了解决交叉问题,可以把Decal按normal方向抬高一定距离。

粒子系统
主要就是发射器参数、生成参数、粒子参数的组合

最后编辑:
作者:wy182000
这个作者貌似有点懒,什么都没有留下。