在解析这个系统前,先说明一下, N3 是一个右手坐标系, 如下所示:
/** N3, right-handed coordinate system
* ^
* y|
* |
* |_______> x
* /
* /
* /z
* v
*/
光照系统和阴影系统历来是两个相生相克的的系统。而且内容非常丰富,在渲染系统中,这两个系统也是各个商业引擎争相称赞炫耀的领域。这里只针对N3在这方面的设计做一个解析。
在解析前,先需要了解一下,现在比较流行的光照的系统模型(不是指光照的颜色理论模型)。具体是:Deferred lighting, Z pre- pass Lighting, Light Pre-pass lighting,
这里引用一个文章:
http://www.openeda.net/forum.php?mod=viewthread&tid=740&highlight=Light%2BPre-Pass 中文翻译后的,关于 Light Pre-pass lighting (N3 用的就是这个模型)
N3 中的光照系统,把光源分为,全局光源,点光源和聚光源。
其中 InternalGlobalEntity 实现的是一个全局有向光,主要用于室外场景渲染和作为室内场景的基准的渲染。全局光,除了一个主要的有向光线外,还包含一个环境色的部分,这个环境色会被应用于处在阴影当中的那些顶点。在渲染的时候,只应该存在一个激活的全局光源。全局光的有向光的部分的方向是沿着光源的变换矩阵的负z轴。变换矩阵的缩放和坐标对于全局光来说没有任何影响。(一个变换矩阵,可以分解成三个子矩形,平移矩阵(对应一个坐标), 缩放矩阵(对应缩放因子),旋转矩阵(对应方向)。对于全局光来说,只有旋转矩阵会影响到它)。
其中 InternalPointLightEntity 对应的是点光源, 点光源需要的是就是一个坐标点, 光的方向,范围
其中InternalSpointLightEntity 对应的是聚光源, 需要坐标点,方向,范围,锥体的角度幅度
对于所有光源都可以设置的参数主要就是:位置,方向,颜色,是否投射阴影。
N3 整个系统对于最大的光源数目是有限制的,对于最大的投影数目也是有限制的,建议场景中投射阴影的光源最好只有一个,光源数据不要超过8个。N3引擎的代码中还限定了一个常量,相机和光源的总数不能超过64。
在细节描述具体的光照算法前,我们先整体了解在N3的整个渲染流程中,光照相关的渲染是一个什么样的流程:
我们从 view 的 render 开始看,
- 1. 是光照服务器和阴影服务器的初始化:
lightServer->BeginFrame(this->camera);
shadowServer->BeginFrame(this->camera);
这一步,其实只是记下相机,并编辑帧渲染的开始
- 2. 确定当前帧,需要计算到的光源,也就是查找摄像机能够看到的光源,
this->ResolveVisibleLights();
深究一下这个函数,其实他是把当前相机看到的所有光源执行一个
void
InternalAbstractLightEntity::OnResolveVisibility()
{
if (this->GetCastShadows())
{
// maybe cast shadows
ShadowServer::Instance()->AttachVisibleLight(this);
}
else
{
// casts no shadows by default, can go directly into lightserver
LightServer::Instance()->AttachVisibleLight(this);
}
}
这个就会把光源加入光源管理器和投影管理器。
注意到一个细节,对于这一趟,那些投射阴影的光源并没用加入光源管理器中,是的,这些灯光是在另外一个地方加入光源管理器的。
void
ShadowServerBase::EndAttachVisibleLights()
{
n_assert(this->inBeginAttach);
this->inBeginAttach = false;
// @todo: sort shadow casting light sources by priority
this->SortLights();
// set only max num lights to shadow casting
IndexT i;
IndexT countLights = 0;
for (i = 0; i < this->localLightEntities.Size(); ++i)
{
if (countLights < MaxNumShadowLights)
{
this->localLightEntities[i]->SetCastShadowsThisFrame(true);
// attach light to light server with correct shadow casting flag
LightServer::Instance()->AttachVisibleLight(this->localLightEntities[i]);
}
else
{
this->localLightEntities[i]->SetCastShadowsThisFrame(false);
// attach light to light server with correct shadow casting flag
LightServer::Instance()->AttachVisibleLight(this->localLightEntities[i]);
this->localLightEntities.EraseIndex(i);
--i;
}
countLights++;
}
}
在投影管理器加载完当前帧投射阴影的灯光后,把在这里设置好投射阴影标准后,加入灯光管理器。
- 3 根据光源信息,生成shadow map。
void
SM30ShadowServer::UpdateShadowBuffers()
{
n_assert(this->inBeginFrame);
n_assert(!this->inBeginAttach);
// update local lights shadow buffer
if (this->localLightEntities.Size() > 0)
{
this->UpdateLocalLightShadowBuffers();
}
// update global ligth parallel-split-shadow-map shadow buffers
if (this->globalLightEntity.isvalid())
{
//this->UpdatePSSMShadowBuffers();
}
}
这里的shadowmap 会分为全局光和点光聚光。 N3在代码中关闭了全局光的阴影生成。
- 4 接下来就是光照计算的各种pass,也就是 light pre-pass 的具体计算
这里贴一下N3中配置的 light pre- pass 的光照计算的渲染批次:
- 4.1 生成solid 的 GBuffer
<!-- render the normal-depth pass -->
<Pass name="NormalDepth" multipleRenderTarget="GBuffer" shader="p_depth">
<Batch shader="b_empty" type="Solid" shdFeatures="NormalDepth" nodeFilter="Solid" sorting="None" lighting="None"/>
</Pass>
- 4.2 生成solid 的 LightBuffer
<Pass name="Prelight" renderTarget="LightBuffer" shader="p_prelight" clearColor="0.0,0.0,0.0,0.0">
<ApplyShaderVariable sem="NormalBuffer" value="NormalBuffer"/>
<ApplyShaderVariable sem="DSFObjectDepthBuffer" value="DSFObjectDepthBuffer"/>
<Batch shader="b_empty" type="Lights"/>
</Pass>
- 4.3 生成 alpha 的 GBuffer (半透明物体的深度缓存和不透明物体是不一致的)
<!-- render the normal-depth pass for alpha lighten objects -->
<Pass name="NormalDepthAlpha" multipleRenderTarget="AlphaGBuffer" shader="p_depth">
<Batch shader="b_empty" type="Solid" shdFeatures="NormalDepth" nodeFilter="AlphaLit" sorting="None" lighting="None"/>
<!-- <Batch shader="b_empty" type="Alpha" shdFeatures="NormalDepth" nodeFilter="ParticleLit" sorting="None" lighting="None"/> -->
</Pass>
- 4.4 生成 alpha 的 LightBuffer
<!-- render the pre-light pass (assumes that a fullscreen global light is rendered first) -->
<Pass name="PrelightAlpha" renderTarget="AlphaLightBuffer" shader="p_prelight" clearColor="0.0,0.0,0.0,0.0">
<ApplyShaderVariable sem="NormalBuffer" value="AlphaNormalDepthBuffer"/>
<ApplyShaderVariable sem="DSFObjectDepthBuffer" value="AlphaDSFBuffer"/>
<Batch shader="b_empty" type="Lights"/>
</Pass>
光照流程基本就是上述描述的。其中涉及几个概念,下深入具体的光照运算前,需要题记一下:
[补充说明 阴影贴图(shadow map)]
要理解N3的阴影绘制就需要先理解阴影贴图。 [http://baike.baidu.com/view/1510558.htm]
Shadow Map 一种用于生成实时阴影的技术。另外一种是Shadow Volume(原理复杂,编写复杂,运算的复杂度与场景复杂度有关)
Shadow Map的基本实现方法:
1、将场景的深度值预先渲染到 以光源位置为原点、光线发射方向为观察方向的投影坐标系中,形成深度纹理。
2、再次渲染场景的过程中,将每个片断(像素)变换到前述眼坐标系中,并缩放到[0,1]的范围内以便查询纹理。
3、以当前片断在眼坐标中的S、T坐标查询深度纹理获得深度值,将此深度值与当前片断的R坐标进行比较,若R坐标大于深度值,则当前片断在阴影中;否则当前片断受光照。
上述是基本原理,希望能够理解。 但令人失望的是,这种方法只适合于灯类型是聚光灯(Spot light )的场合。
如果灯类型是点光源(Point light)的话,则在第一步中需要生成的不是一张深度纹理,是一个立方深度纹理(cube texture)。
如果灯类型是方向光(Directional light)的话:第一步要做的工作是:
1、需要把视点(camera,view)的视椎体(camera frustum)搬到光源的view space
2、求得view matrix的各个参数:farZ参数为在view space中视椎体的maxZ-minZ;nearZ为0.0;upVector是方向光的任意一个垂直向量;lookAt是视椎体的“质心”
3、计算view matrix,把veiw matrix搬到平行投影坐标系(orthographic projection space)
Shadow Map可以正确地形成自阴影,但会出现几种失真。
第一种失真是阴影边缘有锯齿。这容易明白,主要是因为深度纹理的分辨率有限。
第二种是阴影内部甚至是整个场景都有不规则阴影。这是因为深度纹理每一个像素点的精度有限,当这个深度像素点在pixel shader里和当前处理的点做比较时,由于这两点的z都很相近,产生z-fighting。可以通过在做比较时设置一个z偏移(即把这两点人为的分开一点距离)来避免z-fighting;也可缩小投影视椎体大小(即减小fov,减小Zn和Zf的距离,特别的,尽量增大Zn,原因请参考投影矩阵的原理),提高深度纹理像素点的数值大小,从而提高精度。 此外还受深度纹理尺寸的限制,所形成的阴影边缘锯齿较严重。需要进行模糊处理,甚至是半影处理。
N3 的 阴影贴图的渲染
待续