最近拿到SpeedTree资料,开始学习,并用到项目里去.
1. 该插件的特点:
api无关。它本身只是数据结构和逻辑架构,没有任何渲染语句子,因此为了把它应用到自己的引擎里,需要为之添加渲染相关的语句。而根据sdk的讲解,推荐用户为之搭建中间架构,用来联系SPEEDTREE与自己的引擎。这样做起码有两点好处,搭建的中间架构(也推荐别加任何api相关的语句),因此,即使你以后换了api(譬如从gl换成dx),中间架构还是可以继续沿用的。还有一个好处就是,当speedtree更新版本的时候,你也无须修改你的引擎,而只需要修改相对简单而且稳定的中间架构。
2. 该插件的具体特性:
注意,下面具体特性分析都是基于SDK里一个叫“DirectX9”的例子进行的,在这个例子里,它给出了最基本的使用方法,同时也向用户展示了它的基本特性。
A. 树的基本渲染
通过大场景的测试,DP的个数大致是树木棵数的两到三倍。详细分析下,发现
它一棵树分三部分绘制:树干和大树枝(branches),小树枝(fronds),树叶(leaves)
Branches:使用模型来绘制
Fronds:使用两个十字交叉的面模拟小树枝,为了节省三角形。
Leaves:使用billboard方式绘制,这样就能产生视觉效果比较好的叶子了。
它这样划分是出于以下三方面的考虑:这几部分的渲染状态不一样,动画的状态不一样,做LOD的时候也不一样。具体看下面的介绍。
B. 树的阴影系统
它包括两方面的阴影。首先是树干上的阴影。其次是整棵树在地面的投影。
树干的自阴影(self shadow)是预先生成的,至于生成的算法,可能是可以根据可穿透的光线跟踪,也可能是结合shadow map的逐象素地生成光照贴图(把树干的面都展开后,在对应的地方画上阴影).有了该光照贴图,那渲染树干的时候就可以跟树干本身的纹理进行混合产生比较真实的效果。
而整棵树在地面的透影子,则是使用一个矩形画出来的,阴影贴图也是预先生成好。渲染的时候浮在地面。
C. 树的动画
树的三部分的动画状态都是不一样的。这对优化有极其重要的作用。风小的时候,或是树离眼睛比较远的时候,可以不动树枝,而只是动树叶。而具体他们是怎么动的:
树叶的动画:就是一个billboard的来回平移以及他本身绕视坐标系统Z轴的转动。
树枝的动画:通过它引擎本身计算出来的矩阵进行动画。
而至于它具体怎么渲染动画的,它提供了基于CPU和GPU的方法。
基于CPU的方法是:创建顶点缓冲的时候, 使用D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY标记(这种方法能提高CPU修改和更新该缓冲的速度),渲染的时候实时更新顶点位置。
基于GPU的方法是:通过自定义的顶点shader程序进行,更新动画的时候,向shader传递常量数组。
D. 树的光照
它可以打开和关闭实时光照,对于实时光照,树干部分又分两种情况,对于没有法线贴图的树干,使用per-vertex的光照。而对于有法线贴图的,则使用per-pixcel光照。至于给不给树干渲染法线贴图,则根据具体的程序决定。
而对于树叶的渲染,因为它是一个billboard,因此也无法通过其法线来计算光照。它其实是根据这个 billboard的位置来确定其亮度的。通过把整棵树当成一个球来分析,而每个billboard的位置就相当于是球上的一点,结合光的方向,计算出该点的亮度。
E. LOD的特点
其强大的LOD系统,为实现大规模的场景提供了有力的支持。这里的LOD分三方面:顶点的LOD,纹理的LOD,动画的LOD。
(1) 顶点的LOD:首先是针对树干,因为这里的树干是实实在在的模型。至于树干的建立,它里面是采用贝塞尔曲线来描述整个mesh的,贝塞尔曲线的描述方式无疑给即时高效率的LOD计算提供了可行性。同时这还针对树枝,远了之后,小树枝就不渲染了。到了一定的距离的时候,整棵树其实就变成一个billboard了。
(2) 纹理的LOD:树干上在最高精度的时候会有三套纹理:基本纹理,光照贴图,法线贴图。随着LOD的进行,可以依次减去法线贴图,光照贴图,最后是本身贴图,最后只为树干渲染一种颜色。
(3) 动画的LOD:现在有三种动画,大树枝(模型)的动画,小树枝(两个交叉面)的动画,以及树叶的动画。随着LOD的进行,依次去掉大树杆的动画,小树杆的动画,最后是树叶的动画。这也是符合视觉效果的。
F. 文件系统
用场景来分析的话,一个场景是.stf文件(Speed Tree Forest).该文件描述了每棵树的相关属性。而一棵树是通过一个.spt(Speed Tree)文件来描述的.用文本编辑器打开,就能看到里面记录了该树的所有信息。而该插件为此开发了配套了树木编辑器材。使用该编辑器,打开.spt文件之后,就可以对该树进行浏览以及编辑。
3.Speedtree使用实践
它提供给用户的一个最主要的类就是CSpeedTreeRT.这是一个speedtree对外界的接口,从SpeedTreeRT.h中可以看到,这个类其实是包括了该插件的核心类.因此,我们在使用该插件的时候,其实全都是通过这个接口。
譬如CSpeedTreeRT::SetCamera(eye, viewDir),通知它内部现在的摄像机的信息,然后它内部就根据这些信息计算出正确的billboard.
而如何加载一棵树呢?使用CSpeedTreeRT::LoadTree(const char *treefile);输入一个”.spt”文件,然后我们设置光照和风效果的方法如CSpeedTreeRT::SetBranchWindMethod,SetFrondWindMethod,SetBranchLightingMethod,SetLeafLightingMethod, SetLodLimits等,接着执行CSpeedTreeRT::Compute(),然后它里面就开始进行黑盒处理,最后我们就可以获取其几何数据(CspeedTreeRT::GetGeometry)进行渲染。获取之前还可以手动去设置LOD级别CSpeedTreeRT::SetLodLevel,然后你获取到的就是经过LOD处理的几何数据。
不过有一点需要要注意的是,speedtree里面用的是右手坐标系(尽管它说可以通过define Y_UP来改变坐标系统,但我没发现define改了之后有什么变化,很奇怪)。笔者开始的时候完全没注意到这点,发现搬到自己的架构后,树全都是横着的。当时死活发现不了问题,就去旋转每棵树。然后又发现那些树叶也无法正常地旋转成billboard,又查了很久。后来终于发现,是因为speedtree内部使用右手坐标系进行计算。而我的架构是使用左手,这样一来,连传给speedtree camera的数据都要修改了, CSpeedTreeRT::SetCamera(eye, viewDir),其中的eye,eyeDir,都得经过变换再传进去:
float3 viewDir=pCamera->GetViewDir();
float3 eye=pCamera->GetEye();
float afDirection[3];
afDirection[0] = viewDir.x;
afDirection[2] = viewDir.y;
afDirection[1] = -viewDir.z;
CSpeedTreeRT::SetCamera(eye, afDirection);
4.把speedtree加到自己的引擎中去
以上所说的CSpeedTreeRT接口,笔者在使用的时候都是让一个CSpeedTreeRT对象汇聚到自己设计的一个tree类里。通过这种方式来封装speedtree,搭建中间架构。CSpeedTreeRT这接口也许多静态函数,譬如SetCamera,参照它的DEMO,
直接“CSpeedTreeRT::SetCamera(eye, eyeDir);”但要实现完美地跟自己的引擎相结合,也并不是一件容易的事情。主要是,自己的引擎本来就有一套完整的渲染系统,LOD系统,动画系统,而且跟speedtree的方式也不一样。一个极端的做法就是,对于SpeedTreeRT,屏蔽其实时计算,而是根据自己引擎的系统计算,这样的话, 其实是只利用了SpeedTree的数据结果了。而另外一个极端就是,不管 speedtree和自己引擎的关系,只保留简单的耦合,各自使用各自的系统,只是让他们的渲染行为(LOD,光照效果等)保持一致性。至于更好的办法,笔者也是在研讨中,我非常希望能跟读者进行探讨,这也是笔者写本笔记的动机之一。