JefferyZhou

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

在解析这个系统前,先说明一下, 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 中的光照系统,把光源分为,全局光源,点光源和聚光源。

Image

其中 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 的 阴影贴图的渲染


待续

posted on 2012-09-24 16:45  JefferyZhou  阅读(331)  评论(0编辑  收藏  举报