基于需求驱动和OL-System的大规模城市建模

黄友航 梁成 陈春华 陈学伟

scutparttimejob@126.com aliceliang@163.net musalan@msn.com suppermandog@sina.com

华南理工大学计算机科学与工程学院(510640)

clip_image002

图1. 衍变后期城市常系数τ=1.24,fractal d=2.13d效果图

摘要

伴随着虚拟现实、计算机真实感图形学和人工生命等领域的发展,建筑群的建模和模拟吸引了国内外不少学者的研究兴趣,本文探讨了一种通过模拟细胞为了自身发展主动改变环境的过程来模拟城市发展和演化,探讨了基于OL-System下的城市区域迭代细分方法,提出了两级道路样式城市框架,可以呈现城市的发展过程。

关键词:

需求驱动,寻路,OL-System,道路样式

引言

城市是人类文明的象征,是财富生产、积累、传播中心,工业革命后的城市化进程则是当今世界环境和发展问题的重要组成。研究城市化的动态发展过程有重要科研价值和实际价值,随着计算机三维技术大发展,真实感图形学和虚拟显示的迅速崛起,城市发展和衍变过程的三维表现已逐渐收到重视。本文提出了带有人工生命特征的基于GeoCA模型和OL-System文法的大规模城市建模与衍变方法。具有自繁衍、自组织、自敏感、自适应等特征。在我们的实验城市:兰州和武汉中,我们实现了从t=2.1e的小镇衍变到t=44.2e带有30000多建筑物的衍变,并以60FPS以上协同稳定地实时渲染。我们的方法的主要有以下优点:

l 统一表示城市发展中的各种影响因素。使用影响力灰度图来表示各种复杂的影响,反映了各种影响力的全局叠加作用。

l 由内在需求驱动发展,对城市发展的内在原因进行了建模,能够解释道路生成的根本原因—对降低成本的需求。

l 细胞受需求驱动,可以主动地改变环境来适应自身的发展。

l 引入基础道路样式。能够生成人工规划出的城市模型

clip_image004

衍变初期t=2.1e 衍变中期t=20.4e

clip_image006

衍变中后期t=29.3e 衍变后期t=44.2e

注:t为我们自行定义的衍变阶段时间单位,对应WorldExplorer设定的一段模拟时间

图2. OL-system文法下的城市衍变

虚拟城市的建模和仿真一直是国内外研究的热点,城市发展的普遍规律以及如何使虚拟的城市模拟出真实城市的发展过程,也是我们研究的重点。本文主要由以下几个方面组成:首先我们介绍了城市场景建模的大致流程。然后我们提出了一种基于需求驱动的城市生成方法,其中我们对城市发展的根本原因进行了探讨。鉴于以往的研究方向往往集中在环境对城市的单方面影响,我们引入了社区的发展与环境相互影响的模型。在各种道路样式生成的根本原因方面我们提出了一种新的道路生成规则。接下来为了表示城市中各个区域之间的相互影响。我们以影响力灰度图来表示区域之间复杂的影响。

clip_image008

城市常系数τ=1.24,fractal d=2.13

图3. 城市衍变后期线框图

1. 相关工作

模拟城市空间演化的模型很多, 如空间相互作用模型、系统动力学模型、元胞自动机等,其中空间相互作用模型由瑞典学者T.Hagerstrand于1953年在其论文“作为空间过程的创新扩散”中首次提出,最有代表性的是1966年R.A.Grain提出的格瑞-劳利模型,,是基于牛顿力学的物理模型,是一种静态的城市模型,无法模拟出城市的动态发展衍变;系统动力学模型最初由美国麻省理工学院的Jay W.Forrestr教授于1956年提出,可以分析复杂系统的机构和功能,但其基于大量平衡条件下建立的静态的微分偏微分方程模型,不能反映城市空间演化的时空动态变化。而元胞自动机模型是人工生命理论的一个重要分支,具有人工生命自组织、自繁衍、自敏感、自适应的特征,是基于离散的时间、空间和状态的建模方法,更适于城市这样一个离散的复杂系统。它具有以下几个特点:(1)同质性、齐性:即每个细胞都是整齐排布并遵循同样的规则。(2)空间离散:细胞是分布在离散的元胞空间上。(3)时间离散:系统演化是按照间隔的时间分步进行,区别于微分方程中的连续时间。(4)状态离散有限:细胞的状态只能取有限的几个离散值。(5)并行性:每个元胞的计算都是独立的,相互没有任何影响。(6)时空局部性:细胞的下一时刻的状态取决于临近区域细胞的状态,所以传递信息的速度是有限的。(7)维数高:每个细胞的状态都是这个动力学系统的变量,所以是一个维数高的系统。具有这些特征的元胞自动机模型能够模拟很多复杂系统,但在城市模拟方面传统的CA模型都是基于在邻域(交通、邻居细胞)的影响下细胞的演化过程,无法模拟出较远处的因素的影响,也没有考虑到城市发展中道路也受到细胞的影响,所以生成的道路网与细胞的发展不一致,无法模拟出道路的修建及拆迁的过程。

下面就几类当前常见的城市建模与衍变模拟以表格的作一对比:

 

静态结构模型

社会物理学模性

系统动力学模型

经济规划模型

基于CA 的模型

建模原理

区位理论

运输成本论

工业/ 商业利润最大

牛顿社会物理学理论

统计分析理论

系统论

控制论

突变论

最优化理论

复杂系统论

宏观是微观行为

的总和

对时间的表达

不能/ 静态

不能/ 准动态

动态

静态

动态

对空间的表达

均质平原或忽略空间

粗略分区/均质体

粗略分区/ 无空间差异的均质体

均质体/ 宏观功能分区

完全表达空间个体及其相互作用

模拟主题

宏观结构

整体行为

整体行为

配置优化

微观个体行为和空间相互作用

复杂性

单一子系统

单一子系统/ 综合性模型

宏观综合系统

单一系统

时空复合系统

本文引入了CA模型中有关细胞发展的思想,提出了一套基于需求驱动的城市衍变策略。

2、大规模城市框架建模

下图为我们大规模城市场景建模的大致流程。我们根据特定的数据源或参数,完成整个城市的初始建模。

clip_image009clip_image010clip_image011clip_image012

其中深色图框表示输入的数据和生成的数据

图4. 城市建模流程

我们的数据源于主要有以下一些:

l 数字地理数据

- 数字高程模型DEM

- 正射投影模型DOM

- 建筑物高度数据

l 社会统计学数据

- 人口密度数据

- 区域分布数据(住宅区、商业区、混合区)

- 常见城市道路样式

我们首先由地理源数据生成场景中的地形、水陆分布以及地表的植被,这些是城市发展的自然环境。在生成城市的基本框架时,我们根据具体情况设计了三种生成方案:1.基本道路样式2.用户手动绘制3.载入数据。生成的基本城市框架就作为城市衍变系统的输入。针对自然演化的城市和人工规划痕迹较明显的两种类型城市,我们探讨了人工生命自繁衍需求驱动模型下的城市衍变策略和OL-System文法下的城市衍变策略两种方案。

3、基于需求驱动的城市衍变

需求驱动是本文提出的一种概念,是基于对生物中的细胞在环境中由自身的需求驱动主动改变环境来适应自身发展的过程的模仿,来实现对城市发展等复杂模型的模拟。之所以要提出这个概念是因为首先本系统不同于以往的元胞模型和神经网络系统,前两者是通过元胞或神经元对输入信号的条件反射式的反馈来实现模型和环境的互动,而本文中提到的城市建模是基于细胞的需求驱动的,每个细胞都是有自身发展的需求,可以作出改变环境的行为,而不是对环境的机械反馈;生成城市时,是在细胞要求寻求资源的驱动下生成道路,所以生成的城市道路网是符合资源合理配置的要求的。下图是用于生成城市的细胞系统框架

clip_image014

图5. 需求驱动下的城市衍变框架

在框架中处于核心位置的是细胞,为了在计算机中表示的方便,在实现时我们将细胞的分布抽象为一张棋盘状的密度图,密度图中的每个细胞都对其它细胞有影响力作用,如果在分析每个细胞所收到的影响时都要遍历所有的细胞,将会需要很大的计算量,所以我们将每个细胞的影响力统计到一张总的影响力灰度图上,用来表示每种细胞对其它细胞的有利和不利作用,同类细胞的影响力是相互叠加在一张图上的。以工业区为例,每个类型为工业区的细胞都会对周围环境产生影响,其中提供就业是有利影响,而产生污染是不利影响,影响力以细胞为中心向外扩散衰减。而且,根据密度的和衰减系数的不同,不同细胞的不同影响波及的范围和大小都不同。因此,当某个工业区细胞产生变化时,就要修改相应的影响力图。clip_image016

图6. 密度图、影响力图、道路图

影响力计算公式如下:

clip_image018

其中Icell为细胞影响力常数,Ievir为环境因素影响力常数,D为距离。其中,河流、山地等自然环境因素都对细胞有有利的影响,使得细胞在山水环绕的地方容易生长。下图表述了影响力范围的作用。居民区和工厂区之间的有利和不利作用综合发生作用使得两种区域有最佳发展范围。

clip_image020

左边的居民区和右边的工厂区由于互相的影响力的作用,产生了各自的最佳发展范围(密度最大的格子)

图7. 影响示意图

影响力对受体细胞的满足度会产生影响,满足度的大小决定了细胞是发展繁殖还是萎缩消失,因此影响力的在地理上的分布就是细胞发展适宜程度在地理上的分布。计算细胞的满足率S的公式如下:

clip_image022 (1-1)

其中k1,k2,k3为比例系数。Avan表示其他类型对该类型细胞有利影响,公式中对所有相关的有利影响累积求和。同理,disAvan为不利影响,公式中对所有相关的不利利影响累积求和。recRes为该细胞得到的资源量,needRes为该细胞的需要的资源量,Density为细胞的密度。

确定了满足率后要将满足率S与生存系数t1和繁殖系数t2(t1<t2)比较。当S<t1时,表示细胞的环境不适宜细胞生存,细胞产生衰退,密度减少;t1<=S<=t2时,细胞只增加自身的密度,并不向外扩张;当S>t2时,细胞本身密度增加,同时带动周围的密度增加,达到繁殖的效果。

每一种细胞除了受到影响力的作用,还有对资源的需求,当需要的资源大于提供的资源量时,细胞会去寻找资源,如居民区的资源为工业区(就业需求)和公共设施区(社会需求),而工业区的资源为居民区(员工需求)。细胞寻找资源时,有三种可能的路径:直接连接,沿公路,修路,模拟城市中人们出行的方式(寻找资源时细胞的位置并不改变,因为城市中的建筑物的位置是不改变的)。

在寻路过程中我们定义了寻路成本的概念,其中直接连接表示两个细胞相邻,故成本近似为零。沿公路的成本∝所走的公路长度,∝交通流量的平方,在交通拥堵的市中心成本极高。修路的成本是修建成本与收益的差,有可能是负成本,是负成本时就是最佳选择。修路价值成本计算公式如下:

clip_image024

其中Ka为被道路扩展的细胞影响力图中的有利作用;Na为受被扩展的影响力影响到的受益细胞总数;Cb为修路成本,和地形的坡度有关;Cd为拆除沿路建筑成本。

由于寻路时成本最低原则,新修建的道路会自然地连接上已有的道路来减少修建长度(图8)。由于坡度大的地形修路成本较高,所以山地中不会出现道路。而在城市发展出市中心时,由于中心地段的交通拥挤增加了细胞寻路时的成本,使得外围的社区要绕着市中心外圈修建环城道路,而市中心的细胞对外围资源的寻路就生成了星形的路线(图9)。对“同类”的寻路形成了居民区内的网格小路(图10)。如此生成的道路是城市有机的组成部分,对城市的发展有促进作用。

clip_image026

图8. 最小成本原则使得道路自然地连接在一起

clip_image028

图9. 中心高密度的城市在最小成本原则作用下的结果:星形道路

clip_image030

图10. 对临近的同类区域的寻路形成网格状道路

由此可见,细胞系统在道路建模方面是可以模拟各种道路样式的,因为使用的是同样的规则,基于需求驱动的方法模拟了道路生成的原因:即有连通的需求、最低的成本,所以是一种通用的方法。在城市道路生成的同时,以细胞来模拟的各种社区同时发展,并与街道相互作用影响:社区对街道的使用增加了街道的交通流量,从而加大了公路运输的成本,道路将细胞的影响力沿道路扩大,使得比较远的细胞之间能够相互影响,比如使得城市边缘的工厂可以在市区找到员工,而市中心的社区也能到很远的地方找到工作,从而促进了两个区域的发展。而在修路的收益大于成本时,会拆除掉沿路的建筑,使得社区地布局是动态变化的,能够反映出城市发展中的不断拆迁扩建的过程。所以说在细胞系统中社区和街道是同时发展,相互影响的,不同于先生成道路样式再填充社区的方法,细胞系统可以模拟出城市从一个聚落到一个有多个中心,有附属的乡镇,和齐全的市政设施的巨型城市的过程。

4 基于OL-system的城市衍变

基于需求驱动的城市建模方法可以生成依照规律衍变的城市,但还有很多城市是有着明显的人为规划痕迹的,所以我们还探讨了基于OL-System的城市衍变。在多次实验中,我们发现单纯的随机L-system迭代生成的城市在一定程度上会与真实城市有较大的差距。现实中的城市道路分布及样式在很大程度上受到交通及人口密度的影响,虽然我们可以近似地改变人口密度分布,使Open L-system文法生成符合实际的道路分布及样式,但是,并不是所有的道路都与人口分布密度有一定关系描述。例如,连接两个城市之间的道路,与周围人口关系的密切程度远不如其连接的两个城市人口密度关系的密切程度。因此,本文在随机L-system文法的基础上引入了真实区域样式,从而使随机生成的城市更趋真实。

一旦道路分布模型全部确定下来,整个城市就被分成了一个个小区域,把每个区域的中心点定义为区域建筑物的落点。对于每一个落点,都可以确定包围该区域的多边形,然后根据整个城市的繁荣程度近似地分布设定各个落点区域的建筑高度,以该高度为屋顶生成该区域的柱形建筑外形,即完成每一个落点区域的建筑物几何建模。

根据上述思想我们把本文提出的道路生成方法划分为四个步骤,如图11,各步骤的具体细节在下文将分别介绍。

clip_image031

图11. 城市模型建模与衍变设定流程

建模时,考虑到不同参数值的影响,首先需要区分这些因素究竟是属于图11中提及的属于全局的因素还是局部的因素。下面是一些常见因素的分类,对于这两类参数的不同影响将在本文后面部分详细讨论:

l 全局因素:道路样式、人口密度

l 局部因素:水陆分布、地形高程、街道交叉口

4.1种子分布与基础样式

在初始化时,我们首先确定几个人口稠密的地区,作为种子,然后以种子区域为中心向外部生成分支道路,生成的道路的样式可以完全随机,也可以根据周围环境的实际情况选择合理的基础道路样式。下图给出了我们用到的一些基础道路样式。

clip_image032

a光栅/棋格 b放射状/同心圆 c分支

图12. 基础道路样式

对于大部分的城市,可能只有一个或两个的人口高度密集的中心,那么我们就可以考虑定该地区为中心,选择图b的放射状的基础道路样式,也可以完全不采用样式,从该中心以随机角度及预定的半径参数直接生产大量放射状的干道,从中心向外部辐射式地生成主要分支干道,如果存在一个以上的人口密集区,则将一个密集区辐射范围扩大至另一个密集区。

有些城市存在3个或4个的人口高度密集的中心,那么我们可以考虑先建立几个大的干道连接几个人口稠密中心,这样就将城市分成几块大的区域,然后,我们再对大的区域进行简单的初试分割,即在大的干道上利用迭代生成几条分支道路像外延伸,这样就形成了图c的分支状基础道路样式。此外,还有些比较新兴的城市,城市的人口布局比较规则。如果城市中的人口高度密集的中心比较多或者比较规则,则可以考虑建立如图a的光栅/棋格状基础道路样式,利用一些水平线和垂直线将几个人口高度密集的中心贯穿起来。

根据上述步骤,我们就为城市建立了一个大致的道路模板。在此模板中,我们可以使用一种或多种基础道路样式,把城市中的人口密集区连接起来,并生成一些主要干道,将城市划分成几个大的区域,为城市确定一个基础的轮廓,使得城市可以进一步按照基本样式细化。

clip_image034 clip_image036

图13. 基于基础样式生成的城市道路大致轮廓

4.2 与真实城市相关的二级道路样式

然而如果仅仅采用基础样式与下文提及的随机OL-system文法生成的道路,在很大程度具有其分形自相似本质,与真实城市道路有一定差距。为了生成更接近于真实的城市道路,本文对于各基础样式再采用一些已知的真实城市道路样式作为二级样式模板,替代某些已经生成但与实际有较大差距的道路集,然后对道路集根据文法规则进行迭代,产生更多的子道路(分叉)集合。下表给出了本文涉及到的一些与真实城市很接近的规模化道路样式:

二级样式名称

基础样式

图例

纽约样式

格网井状

clip_image038

巴黎样式

中心辐射

(干道较少)

clip_image040

伦敦样式

中心辐射

(内环为空,

干道较多)

clip_image042

北京样式

中心辐射

(典型的古城式,方形环路,由城中心向外辐射)

clip_image044

莫斯科样式

中心辐射

clip_image046

柏林样式

中心辐射

clip_image047

圣弗朗西斯科样式

高低起伏

(根据高程坡度走向)

clip_image049

图14. 复杂道路形式

通过把多种样式组合使用,我们可以得到更复杂更逼真的城市道路分布。

4.3 基于OL-system文法的区域细节渐进

在生成了城市的大致主要道路干道之后,我们还需要生成各种分支道路,为了使生成的道路能够体现合理的分叉性,本文提出了基于OL-system文法的树型道路迭代生成方法。

Open L-system是一种基于一组产生法则的并行字符串重写算法。每条字符窜包含了一定数量的互相区别的模块,模块会被作为命令来解析。命令的参数保存在模块之中。当我们要写一套复杂的法则系统来创建一个道路区域时,有非常多的参数和条件要实现到L-system当中,很多法则必须重写。新产生的字符及其复杂度增长的很快。每实现一个新的制约条件,许多的法则就需要被重写。这使得可拓展性的实现变得非常困难。因此,文本提出的策略不在每次的生成中设置参数,而是让L-system在每次生成步骤中产生一个通用的模板,不仅可以方便的修改它的法则,而且可以生成时如果要对参数进行重新设置也无需重新生成法则

clip_image051 clip_image053

二级样式生成的道路 对已有道路分析得到的区域

图15.

在L-System基础上我们对区域进行空间剖分,生成的道路将区域逐渐细分。即新增一条道路,将原区域分割成两部分。

clip_image054

图16. 城市衍变框架

上图a是城市大致轮廓中的主干道所围的某一区域,每迭代一次,每条原有的道路就产生1到3个分支道路,随着迭代次数的增加,道路数量不断增加,直到道路密度达到设定的全局基准值,这样通过OL-system和空间剖分对区域添加细节,我们就基本上能够实现整个城市的完全随机生成。

4.4 基于自敏感L-system的局部调整

对于大部分城市来说,我们在大规模随机生成之后,为了使城市样式更好地满足我们的设计,我们需要对局部进行修正。本文通过自适应L-System文法实现该过程。首先检测本路段同其他路段的交叉点,如果发现了交叉点,将剪除该段路,然后生成十字路口。另外,如果系统发现了在给定的街道半径范围内有路段的末端存在,那么它也将修改参数,建立一个新的十字路口。

下面是文本涉及的局部调整的几条规则:

l 两条街道相交生成十字路口

l 路段末端接近十字路口延伸街道至十字路口

l 接近交叉点延伸街道产生交叉

结论

我们的城市生成和显示系统提出了由细胞需求驱动的城市发展模型,模拟了真实城市发展衍变的内在原理,而不仅仅是对城市形态的表面现象的模拟。在我们的模型中道路可以扩展细胞的影响力范围,区别于传统的元胞自动机的时空局部性的特点,将影响每个细胞发展的因素扩展到全局,不再受影响力半径的限制。同时这种设定改变了传统的将道路的影响力与细胞影响力相分离的做法,使道路成为传播影响力的载体而不是独立的影响因素。在道路生成方面,我们将每条道路的生成都看作细胞改变环境的智能行为,使得每一条道路的生成都具有其合理性。在系统的可扩展方面,我们将影响城市发展的各种因素都抽象为影响力图,只要增加不同的影响力参数就可以模拟天气,河流,政策等因素对城市发展的影响。

此外,考虑到现实中很多的城市在人为规划的影响下都有着自己独特的风格,比如一些中心对称的古城,所以在生成一些人为布局的城市样式时,我们引入了OL-System,可以按照我们设定的道路框架生成特定样式的城市雏形,然后在基于需求驱动的城市衍变模型下进行发展,使其在保持城市布局风格的前提下按照自然的规律发展,这样就可以生成既逼真又有特色的城市,从而更加接近真实世界。

参考文献

1. Nobuko kato & Tomoe okuno & Aya okano “An alife approach to modeling virtual cities” IEEE System , Man and Cybernet-ics 1998 1169-1174

2. Nobuko Kato ”Modeling Virtual Cities Using Genetic Algorithms”IEEE Midnight-Sun Workshop on Soft Computing Methods in Industrial Applications Kuusamo,Finland,June 16-18,1999

3. Yoav I H Parish & Pascal Muller “Procedural Modeling of Cities” ACM SIGGRAPH 2001,12-17

4. 周成虎&孙战利&谢一春”地理元胞自动机研究”科学出版社2001

posted @ 2011-09-08 18:32 ~哇哇~ 阅读(83) 评论(0) 编辑
摘要: 面向GPU的多LOD因子的大规模场景可视化策略 张嘉华 梁成 陈利强 陈春华 newzjh@msn.com aliceliang@163.net sailmer@hotmail.com musalan@msn.com 华南理工大学计算机科学与工程学院(510640) 图 1. 多LOD因子控制的大规模地形渲染(C1=1.03,C2=19.55,C3=0.868 下山脉地区三维漫游) ...阅读全文
posted @ 2011-09-08 18:30 ~哇哇~ 阅读(262) 评论(0) 编辑
摘要: 细分曲面的GPU完全实现 张嘉华 梁成 李桂清 (华南理工大学计算机科学与工程学院 广东 广州 510640) 摘要:本文提出一种完全在GPU上实现的细分曲面绘制策略。该方法以扇区为基本单元,利用三种新的枚举坐标对网格顶点进行编码。初始控制网格按扇区分割后,其顶点被编码到一张纹理上,控制顶点的不规则性分离到扇区的同时扇区之间又具有松耦合联系,使GPU像素管线的并行性从片段网格内扩展到整个网格...阅读全文
posted @ 2011-09-08 18:23 ~哇哇~ 阅读(153) 评论(0) 编辑

 

真实感森林的建模与可视化

Modeling and Visualizing a Real Forest

Donald H.House,    Midori Kitagawa-DeLeon    Greg S.Schmidt,    Scott A. Arvin,
德克萨斯州A&M可视化实验室
翻译:张嘉华,newzjh@126.com

   

clip_image002[4]

clip_image004[4]

文章来源:IEEE Computer Graphics and Applications,大规模场景对象渲染技术系列文章(1)

介绍 (Introduction)

在德克萨斯州的A&M可视化实验室,我们最近和计算机科学家、心理学者、艺术家以及其他方面专家合作,共同创造了一个具有高度真实感、计算机生成的森林漫游。在美国农业部(USDA)的资助下,这项工作在亚利桑那州大学心理学实验室进行了实验。研究员希望测定在计算机生成可视化自然场景动画的现实程度,让人们得以了解虚拟环境和现实究竟有多少相似。这项计划将要比较项目在真实情况、图像带、图象漫游、和计算机模拟漫游下的效果。

1. 地点数据和信息
(Site data and information)

亚利桑那州大学科学家调查了一个位于美国犹他州南部自然森林中的风景优美的山坡,并且使用了激光范围搜索设备和GPU技术以定位每一颗树。初始的地形高程数据来自一个ARC/INFO三角网格,我们将它转换为规则网格。这让我们易于应用纹理和位移图到最终的渲染。该地点可视化的区域约占18亩地,150英尺高程,包含1600颗树。我们得到树木的数据文件——每一颗树一行数据。对于每颗树,有位置、高度主干和遮盖直径、类型、针的比例和生长角度。

clip_image006[4]

图1:在该地点平面图的

成阶层的摄像机路径

clip_image008[4]

2. 树木几何定义 (Tree geometry definition)

Bogas分枝对象生成和动画系统生成树木的几何结构。该系统由俄亥俄州大学Midori Kitagawa-DeLeon研发,Bogas作为整个成果的骨干,——它为所有的每一颗个体的树修改树木调查数据为realistic representative几何结构。Bogas使用参数化的算法过程去“生产”有机的分枝结构。为了在Bogas中发育一个分枝,你必须首先定义几何图元元素,它构成对象并且定义了这些元素间的拓扑和几何关系。一个定制的软件工具提供了地点调查数据和Bogas的界面。该软件转换树木调查参数——如树木高度、树干直径、遮蓬半径为一个适当的Bogas参数集,Bogas的输出按椭圆体和多边形定义了树木的几何结构。我们使用额外的定制软件转换这树木几何到Pixar动画工作室的Renderman4软件的RIB文件格式。

图2:作为纹理形式

的个体树木

3. 场景生成过程
(Scene generation process)

为了生成场景,我们首先定义一个摄像机路径,然后渲染每一颗树,并且最终创建动画中每一帧单独的图象。我们设计摄像机路径以模拟一个人沿着连续的路径漫步,在每个预先定义的点停下来,转360度,并且继续沿着路径走。该路径打算模拟真实的森林漫步远足旅行。图1展示了一个带有树木、摄像机路径和旋转点的该地点的布谷图。结合从初始调查得到的树木大小的数据,我们使用Bogas为每一颗树创建几何结构。在该点上,我们为每颗树渲染一个二维图象以创建纹理映射代表在最终图象中远离摄像机摄像机的树(见图2)。对于最终动画中的每一帧,我们创建一个Renderman4的RIB文件。该定制软件从Bogas综合每一颗树的几何结构,树木坐落于最初调查所得的位置,矩形网格来源于地形数据,摄像机路径信息定义每一帧。我们编写定制的Renderman shader为地面和树皮提供位移(displacement)和纹理图。针叶构成Engleman Spruce树叶,同样是由定制的位移(displacement)shader生成的,它转换Bogas输出的简单的椭圆体的针叶为定人信服的针叶从,如图3。

clip_image010[4]

图3 Needle closeup displacement map

4. Managing size and complexity

我们的生成过程,从初始化的地形和树木数据到渲染的每一帧,逻辑地创建了从数据到图象的管线。但是,许多其它实际问题需要解决,不仅仅我们需要解决的问题大小。首先,我们想通过强制(brute force)使用最大可能的计算能力能够快速地给我们结果。我们在德克萨斯州A&M超级计算中心的SGI Power Challenge机器,带有24个每转200MHZ的处理器。我们每个处理器有1G内存空间,4G磁盘空间用于渲染,在几何用户之间共享。即使在这样的大型机器,我们首次尝试创建RIB文件并且试图渲染它们均失败了。RIB文件和里面的模型太大,超过了RAM能够处理的能力,——换来的结果是虚拟内存交换使得渲染非常慢。一些图象根本没有渲染,导致机器在几小时后崩溃。其它图象花费了超过10小时用于渲染,并且RIB文件可能超过500M,最后,我们减少渲染时间到一个范围:从20分钟到2小时,RIB文件大小减少到以下范围:从50M到120M。最终,我们通过小心处理潜在的复杂问题,在努力维持一个高度可见的真实感情况下,完成了渲染。

     

clip_image012[4]

5. 细节层次技术(Level of detail)

我们首先尝试通过每帧减少递交给RIB文件的树木数量以减少复杂度。在三维图形学中,你通常传递整个模型给渲染器,并且让渲染器软件丢弃模型中对于当前摄像机位置不影响图象渲染的部分,此外,所有模型元素通常以全分辨率展现,渲染器必须检测多精细或多粗糙去抽样特定模型元素以得到渲染图象中的合适精度。但是,在我们的问题中,模型中的树木数量太大,每颗树都太复杂了,使得这种标准方法显得不切实际。取而代之,我们预处理模型,传递给RIB文件仅仅那些从当前摄像机位置影响最终图象的树木。图4展示了带有一个三角形高亮标识了摄像机位置和视锥的森林图例。截然不同的颜色代表不同的树木类型:处于视锥内和仅仅超出视锥范围的的树木(绿色),那些完全处于视锥外但它们的阴影可能重叠在视锥内的树木(黄色),其它被剔除的树木(红色)。所有的场景中的树木根据它们距离摄像机的距离被分类为near、mid、far和very far区域。我们使用上述的树木距离区域去决定在渲染树木时在RIB文件中使用那种细节层次(LOD),并且决定那种颜色和位移(displacement)细节。

图4:根据观察视锥分类并剔除树木

图5,一个渲染好的图象,展示了树木是如何从4个不同区域呈现。树木在标识为very far的区域中不以几何模型的形式包含在RIB文件中,取而代之的是,它们表现为一个具有透明度的面向摄像机的矩形多边形,带有不透明的树木二维纹理图在它之上(如图2)。这种技术通常被称为公告牌。我们完全建模剩下的树,但根据与摄像机的距离渲染它们(见表1)。在far区域中的树木通过仅仅实体颜色着色(solid color shader),没有针对树皮和针叶的位移(displacement)贴图。在near区域的树通过全部纹理和表现真实树皮针叶的位移(displacement)贴图渲染。对于中间区域的树,我们在纹理和位移贴图(displacement)之间插值,从全纹理到实体颜色(solid color),从全纹理和全位移贴图到没(none),根据它们距离摄像机的距离,这种插值方案避免动画中由于渐进漫游过程树木在区域间移动所产生的讨厌的可见效果。

clip_image014[4]

图5不同的细节层次

6. 实例化 (Instantiation)

根据视锥预处理模型被证实在处理过程中非常有用,但是在一些动画片段中,场景仍然太大和太复杂难以渲染。所以,我们引入实例化(instantiation)以减少截然不同的树木几何模型数量在一个RIB件中。我们在原始树木数据库中根据类别和高度对对1600颗树进行分类。类别包括幼树(baby tree)和成熟的树(mature tree)以及每颗树是否活的,将要死亡的还是已经死亡的。高度每两米区间为一组。我们小组为每一个类别和高度创建一个模型,结果剩下70种截然不同的几何模型。每个RIB文件包含仅仅特定场景需要的树木几何模型和那个模型应该放在地形中那个位置的信息。实例化方案让我们明显减少RIB文件大小,并且在漫游中以适当的帧速和时间渲染所有场景。但是,着技术确实有一个主要缺点——我们丢失了不同树木之间的差异性。例如,树木宽度和复杂度(叶子数量和枝干数量)变成了静态的。我们相信无经验的观察者不会注意到这些丢失的差异性。当观察作为森林中一个很大的整体时,并且带有针叶和其它添加的细节,那是几乎不可能存在一样的树木。

7. 照明和观察Illumination and shadows

当我们创建合适的几何结构、摄像机动作和表面细节后,我们关注森林中的照明以达到真实地在森林中徒步旅行的效果。我们建模天空作为一个蓝色的椭球体包围着森林。主要的关照来源于7月21日早上10:00时太阳位置所产生的平行光。一个很小的环境光模拟由于反射和大气漫反射引起的光线。最后,一个填充式的光总是处于摄像机位置,并且和观察方向一致的发射方向,提供一些额外的光线和背面光漫反射感觉。我们用Renderman depth-cue shader创建大气效果,它给予摄像机到反射面的距离混合背景色到来自表面的光线反射中。阴影的存在对于最终的动画表现是犹关重要的。图6展示了同样的场景带有三种不同层次阴影渲染的效果。图6a展示了没有阴影的场景,森林显得非常静态,并且不真实——显然是计算机生成的。在图6b,我们看到同样的树带有通过包含仅仅树皮和枝干在阴影计算中的“telephone pole”阴影。我们使用阴影层次在我们的首次尝试渲染全部动画。最终,在图6c,我们看到场景带有全部阴影渲染——包含树叶canopy效果。在创建遮蔽的大区域和象征森林的太阳,消耗了50%更多的渲染时间去包含恰当的telephone pole阴影;从这到全阴影增加了额外的20%渲染时间。但是,可见的真实感带来的冲击是非常重要的以致于我们决定包含全阴影在最终的动画版本中。

clip_image016[4]

clip_image018[4]

clip_image020[4]

图7:各种阴影渲染的森林对比

8、成果 (Production)

那是很难去预测一个场景动画的效果直到它被渲染和以视频帧速回放。整个序列,包含7200帧,包含7个360度回转和7个直线截取。对于动作,为了快速得到一个效果,我们开始每30帧渲染以保证所有的东西合理渲染,我们通过从一帧到下一帧的分解把这些帧放到带上,以观察动作和时间。在成果的鼓励下,我们填充丢失的帧并且以每秒10帧渲染并产生动画。沿着直线路径上截取每一帧,看起来对了,但是需要在360度转弯处更细化。所以我们在转弯处以7.5FPS帧速渲染。不幸运的是,这并没有带来好的效果,由于转弯处会带来令人昏乱(dizzying)效果。为了抵消这种效果,我们渲染转弯处以一系列摄像机位置静止,大约每帧之间30度旋转的10多帧,分解转弯处360度的两帧之间为30度旋转的12帧。这带来了转弯处急速勒马(saccades)的效果。最终,我们解决勒许多在产生森林漫游动画中遇到的困难的问题,并且最终产生了如图7所见的完整动画。我们在NTSC BetaCam 录像带渲染并记录了整个4分钟动画。同时,我们在准备7个动画片段和7次转弯动画的MPEG版本。

clip_image022[4]

图7:最终的效果

9. 结语 (Conclusion)

这项目需要许多工具和技术去解决高度复杂的真实感需求带来的问题。这项目教会我们在自然场景工作中分层次细节层次建模和渲染。我们将会提供地表纹理质量,这可能是我们图象中最缺乏真实感的地方。一个可能的方法将会是使用过程着色(procedural shader)代替位移贴图和纹理映射。令一个方面的改进是包含各种各样的树木颜色表现死亡的树木和森林地表残骸,并且建模环境效果,如基于森林密度的树木形状改变。如果让我们重新再做这个项目,我们会考虑编写一个专门的渲染器,为我们的问题和需求优化,以提高渲染时间。我们得到的最好的称赞将会是我们真实森林场景的工作得到一个研究员花费可观的时间关注,他所看到的图象和动画,截取了森林的灵魂和气息,带给他真实的沉浸感。

鸣谢 (Acknowledgments)

参考文献 (References)

1. D.H. House et al., “Visualizing the Midway Face of the Dixie National Forest,” Proc Siggraph 97 Visual Technical Sketches,ACM Press, New York, 1997, p. 207.

2. M. Kitagawa-DeLeon, “Branching Object Generation and Animation System with Cubic Hermite Interpolation,” J.Vis. and Computer Animation, 1994, Vol. 2, pp. 60–67.

3. M. Kitagawa-DeLeon, “Rendering the Midway Face Forest, Towards High Realism in Environmental Visualizations,”Proc. Resource Tech. 94, American Soc. of Photogrammetry and Remote Sensing, Bethesda, Md., 1994, Vol. 1, p. 231.

4. S. Upstill, The RenderMan Companion: A Programmer’s Guide to Realistic Computer Graphics, Addison-Wesley,Reading, Mass., 1990

posted @ 2011-09-08 18:11 ~哇哇~ 阅读(52) 评论(0) 编辑

特效的批量绘制

张嘉华(newzjh@126.com)

特效系统是游戏中的一个重要组成部分,在场景布谷,角色技能等有广泛应用。一个特效往往包含多种多样的组成元素:粒子系统,公告板,音效,图元轨迹,模型特效,镜头滤镜/震动/模糊,动态光源等。其中以粒子系统和公告板在场景中用得最多,往往一个特效由上述这些元素构成多个轨道,沿着时间轴以不同起始时间(相位)和周期播放。批量绘制和几何实例化(Batching and Geometry Instancing)相信读者已经不太陌生,那么比较困难的是既要满足美术开发时每个实例的多样性灵活性(多种渲染状态同时存在),又要保留批量绘制的高效(不同状态,不同成分的实例一起绘制)。根据我们的实验,在一个场景同时绘制100多个特效,每个特效包含4~10个粒子系统轨道,2~6个公告板轨道能够有好的效果和效率。接下来本文以粒子系统和公共板为例简单介绍一下我引擎中的实现方式。

clip_image002

1. 多个粒子系统的批量绘制

单个粒子系统的实现主要有状态保持和非状态保持两类。状态保持就是粒子系统中的每个粒子每帧都进行更新,根据各样的规则分别更新加速度,速度,位移,这种方式能够让美术在周期内对加速度或速度的变化根据曲线或者规则变化,也能够在周期中中途临时改变粒子的走向等,但是这种方式往往需要用若干RenderTarget纹理保存粒子当前位移,速度等状态,通过渲染到纹理来每帧更新纹理中每个粒子的这些状态;另外一种方式就是非状态保持,粒子的状态在每帧由公式根据平均加速度,最大速度,最小速度计算出当前的位置,这种方式比较利于在GPU的Vertex Shader中为每个粒子直接用中学物理公式:Pt=P0+vt+0.5*a*t*t计算出位置,而不用像状态保持方式那样通过Shader Model 3.0支持的tex2DLod这样的指令读取上一帧的状态。在我的游戏中,由于临时改变状态的行为比较少,因此单个粒子系统的实现只在GPU中采用了比较简单和易于实现的非状态保持方式。

1.1 多个粒子系统批量绘制的需求

无论状态保持和非状态保持,对于有经验的3D程序员来说,实现都不会太困难,而比较困难的是跨系统的粒子之间如何也进行批量绘制。那么接下来研究下多个粒子系统批量绘制的需求,也就是看看粒子系统之间究竟有多少差异需要进行提取和合并:

1, 不同的粒子系统采用不同的贴图,贴图需要合并

2, 不同的粒子系统下面这些参数可能不一样:最小速度,最大速度,最小角速度,最大角速度,最小生命周期,最大生命周期,每次发射粒子数,粒子发射间距等

3, 粒子系统跟帧缓冲的混合模式不一样:0暗的叠加(srcblend= srcalpha, destblend=invsrcalpha),1亮的叠加(srcblend=srcalpha,destblend=one)

4, 粒子的朝向不一样:0朝向某个法线,1朝向镜头

5, 粒子的开始相位和随机数生成不一样

1.2 Constants Instancing

根据这些需求,我们开始进行合并和批量绘制,采用的方法是GPU Gems2 Chapter3里面提到的四种实例方法的第三种:Constants Instancing
http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter03.html)。既提供一些对不支持硬件Instancing(不支持SetStreamSourceFreq)的向后兼容,也提供了能够每帧通过Constants设置粒子系统参数的灵活性。

1.3 顶点缓冲和索引缓冲的创建填充

首先,我们需要创建足够存储多个粒子系统的所有粒子的顶点缓冲和索引缓冲:

ret=pd3dDevice->CreateVertexBuffer(MAX_PARTICLE_SYSTEM_BATCH*MAX_PARTICLE_NUMBER*4*sizeof(PNCT1Vertex),0, D3DFVF_PNCT1Vertex,D3DPOOL_MANAGED, &g_pVB, NULL );

ret=pd3dDevice->CreateIndexBuffer(MAX_PARTICLE_SYSTEM_BATCH*MAX_PARTICLE_NUMBER*6*sizeof(int),0,D3DFMT_INDEX32,D3DPOOL_MANAGED,&g_pIB,NULL);

MAX_PARTICLE_SYSTEM_BATCH是定义每批次绘制的最大粒子系统数量的宏

MAX_PARTICLE_NUMBER是每个粒子系统中最大的粒子数量

所有批次的粒子系统都是共用这个顶点缓冲和索引缓冲,接下来就是填充顶点和索引数据,这里每个顶点的position存的是构成每个粒子的四边形面片的每个顶点在对象空间中的坐标,Shader的语义是POSITION0;每个顶点的索引index的三个分量存的是i粒子系统序号,j单个粒子系统中粒子序号和顶点在粒子面片4个顶点中的序号,语义是BLENDINDICES;每个顶点的uv存放的是纹理坐标,这里减少了0.01f是为了在合并后的纹理采样时看不到纹理间的裂缝。

int vertexcount=0;

//设置顶点

PNCT1Vertex* pVertices=NULL;

ret=g_pVB->Lock(0,0,(void**)&pVertices,0);

vertexcount=0;

for(int i=0;i<MAX_PARTICLE_SYSTEM_BATCH;i++)

{

for(int j=0;j<MAX_PARTICLE_NUMBER;j++)

{

pVertices[vertexcount+0].position=float3(-1.0f,-1.0f,0.0f);

pVertices[vertexcount+0].index=int3(i, j,0);

pVertices[vertexcount+0].uv=float2(0.01f,0.99f);

pVertices[vertexcount+1].position=float3(-1.0f,1.0f,0.0f);

pVertices[vertexcount+1].index= int3 (i, j,1);

pVertices[vertexcount+1].uv=float2(0.01f,0.01f);

pVertices[vertexcount+2].position=float3(1.0f,-1.0f,0.0f);

pVertices[vertexcount+2].index= int3 (i,j,2);

pVertices[vertexcount+2].uv=float2(0.99f,0.99f);

pVertices[vertexcount+3].position=float3(1.0f,1.0f,0.0f);

pVertices[vertexcount+3].index= int3 (i,j,3);

pVertices[vertexcount+3].uv=float2(0.99f,0.01f);

vertexcount+=4;

}

}

ret=g_pVB->Unlock();

//设置索引

int* pIndices=NULL;

ret=g_pIB->Lock(0,0,(void**)&pIndices,0);

vertexcount=0;

for(int i=0;i<MAX_PARTICLE_SYSTEM_BATCH;i++)

{

for(int j=0;j<MAX_PARTICLE_NUMBER;j++)

{

pIndices[0]=vertexcount+0;

pIndices[1]=vertexcount+1;

pIndices[2]=vertexcount+2;

pIndices[3]=vertexcount+2;

pIndices[4]=vertexcount+1;

pIndices[5]=vertexcount+3;

vertexcount+=4;

pIndices+=6;

}

}

ret=g_pIB->Unlock();

1.4 渲染时粒子系统参数的传递

接下来就是在渲染函数ParticleSystemManager::Render()把多个粒子系统的参数一次送到GPU的常量寄存器。首先计算总共有多少个批次batchnumber,用总共要绘制的特效系统实例数除以MAX_PARTICLE_SYSTEM_BATCH等到。接下来循环对每个批次中的各个粒子系统,计算该批次中粒子系统数量batchsize,等到每个粒子系统实例的数据指针pEntity,得到实例中用到的特效元数据指针pEffectElement和粒子系统数据指针pParticleSystem,接下来把这些参数编排到一个float4数组构成的结构ParticleSystemParameters,把这个数据通过
g_pEffect->SetFloatArray("batchdata",(float*)(&ParticleSystemParameters[0]),28*batchsize)一次设置到常量寄存器,通过ret=g_pEffect->SetFloat("batchstart",(float)batchstart)设置这批次的起始粒子系统序号,g_pEffect->SetTexture("particletexture",GEffectManager::GetTexture())设置合并后的特效纹理

static GParticleSystemParameter ParticleSystemParameters[MAX_PARTICLE_SYSTEM_BATCH];

UINT cPasses2=0;

ret=g_pEffect->Begin(&cPasses2,D3DXFX_DONOTSAVESTATE);

int batchnumber=nEntityNum/MAX_PARTICLE_SYSTEM_BATCH+1;

for(int batchindex=0;batchindex<batchnumber;batchindex++)

{

int batchsize=MAX_PARTICLE_SYSTEM_BATCH;

if (batchindex==batchnumber-1)

batchsize=nEntityNum%MAX_PARTICLE_SYSTEM_BATCH;

int batchstart=batchindex*MAX_PARTICLE_SYSTEM_BATCH;

for (int i=0;i<batchsize;i++)

{

GEffectElementEntity* pEntity=&pElementEntities[batchstart+i];

GEffectElement* pEffectElement=&pEntity->EffectElement;

GParticleSystem* pParticleSystem=(GParticleSystem*)(&pEffectElement->data[0]);

ParticleSystemParameters[i].parameters[0]=float4(vTranslation.x,vTranslation.y,vTranslation.z,pParticleSystem->m_fEmissionInterval);

ParticleSystemParameters[i].parameters[1]=float4(pParticleSystem->m_vMinVelocity.x,pParticleSystem->m_vMinVelocity.y,pParticleSystem->m_vMinVelocity.z,pParticleSystem->m_fParticlesPerEmission);

ParticleSystemParameters[i].parameters[2]=float4(pParticleSystem->m_vMaxVelocity.x,pParticleSystem->m_vMaxVelocity.y,pParticleSystem->m_vMaxVelocity.z,pParticleSystem->m_fMinLifeSpan);

ParticleSystemParameters[i].parameters[3]=float4(pParticleSystem->m_vAcceleration.x,pParticleSystem->m_vAcceleration.y,pParticleSystem->m_vAcceleration.z,pParticleSystem->m_fMaxLifeSpan);

ParticleSystemParameters[i].parameters[4]=float4(fSize,pParticleSystem->m_fWidthRatio,pParticleSystem->m_fMinAngularVelocity,pParticleSystem->m_fMaxAngularVelocity);

ParticleSystemParameters[i].parameters[5]=float4(pParticleSystem->m_fMinRadius,pParticleSystem->m_fMaxRadius,pParticleSystem->m_eBlendModel,(float)pEffectElement->nTextureIndex);

ParticleSystemParameters[i].parameters[6]=vColor;

}

ret=g_pEffect->SetFloat("batchstart",(float)batchstart);

ret=g_pEffect->SetFloatArray("batchdata",(float*)(&ParticleSystemParameters[0]),28*batchsize);

ret=g_pEffect->SetTexture("particletexture",GEffectManager::GetTexture());

for (int iPass = 0; iPass < (int)cPasses2; iPass++)

{

ret=g_pEffect->BeginPass(iPass);

ret=pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0, 0 ,batchsize*MAX_PARTICLE_NUMBER*4, 0 ,batchsize*MAX_PARTICLE_NUMBER*2);

ret=g_pEffect->EndPass();

}

}

ret=g_pEffect->End();

1.5粒子系统批量绘制的Shaders

接下来,我们开始编写FX文件来实现批量绘制。在Vertex Shader中,首先从BLENDINDICES语义中读入每个顶点的三个索引: i粒子系统在这批次中的序号,j粒子在单个粒子系统中的序号,k顶点在粒子面片中的序号。接下来根据序号从常量参数batchdata读出单个粒子系统的所有参数parameter0~parameter7。接下来一个很重要的事情就是为这些粒子根据序号j+i+batchstart作为种子编写的线性同余法产生确定性的伪随机数,用于后面给每个粒子在系统约束的最小最大速度,最小最大面积等之间分配数值,由此得到4个伪随机数randnum0~randnum4,并把前3个伪随机数写成3维向量randthree方便后面运算。接下来从parameter0~parameter7提取需要的参数。然后根据这些参数构造一系列公式计算粒子的具体当前参数,这些公式是本文的主要贡献之一。

粒子周期内时间:

Half ftime=saturate ( fmod ( time – floor ( j/ fParticlesPerEmission ) *fEmissionInterval , fTotalEmissionInterval ) / fLifeSpan ) * fLifeSpan;

上面这条公式首先对当前绝对时间time,根据初始相位进行偏移。由于每次发射fParticlesPerEmission 个粒子,那么j/ fParticlesPerEmission 就是第j个粒子所处的发生起始次数,乘以粒子发射间隔fEmissionInterval 就得到该粒子的初始相位floor ( j/ fParticlesPerEmission ) *fEmissionInterval。把这个相位叠加到绝对时间,对该粒子总的发射间隔fTotalEmissionInterval求模就得到该第j个粒子的发生区间。这里fEmissionInterval是从起始时间算起单个粒子系统中每批粒子发射间隔,fTotalEmissionInterval则是第j个粒子两次发射之间的间隔,因为粒子在GPU中周期到了会循环再利用。

粒子的循环再用周期:

half fTotalEmissionInterval = max ( MAX_PARTICLE_NUMBER * fEmissionInterval / fParticlesPerEmission , fLifeSpan );

fTotalEmissionInterval是第j个粒子两次发射之间的间隔,当该粒子的生命周期大于同一个系统中所有粒子循环再用的间隔,则以该粒子的生命周期作为循环再用周期,否则以同一个系统中所有粒子循环再用的间隔作为该粒子循环再用的周期。

粒子的生命周期:

half fLifeSpan=lerp(fMinLifeSpan,fMaxLifeSpan,randnum3);

粒子的生命周期根据前面算出的伪随机数在该系统的最小生命周期和最大生命周期作线性插值得到,由于伪随机数randnum3是根据粒子系统的序号和系统中的粒子序号j+i+batchstart作种子得到的,因此每个粒子得到的这个随机数都不一样,但又是确定的,也就是说只要粒子系统中的粒子序号j,粒子系统序号i,这批次中粒子系统起始序号batchstart都确定,这个随机数也就确定,生命周期也就随之确定。

粒子的速度:

half3 velocity=lerp(minvelocity,maxvelocity,randthree);

类似地,根据三维伪随机向量randthree在粒子系统的最小最大速度之间线性插值出该粒子的速度,这个得到的速度向量velocity也是关于j,i,batchstart具有确定性的。

粒子的角速度:

half fAngularVelocity=lerp(fMinAngularVelocity,fMaxAngularVelocity,randnum3);

half fAngular=fAngularVelocity*time;

half2 rotatevec=half2(cos(fAngular),sin(fAngular));

initpos.xy=half2(initpos.x*rotatevec.x-initpos.y*rotatevec.y,initpos.y*rotatevec.x+initpos.x*rotatevec.y);

类似地,根据伪随机数randnum3在粒子系统的最小最大角速度之间线性插值出该粒子的角速度,这个得到的角速度fAngularVelocity也是关于j,i,batchstart具有确定性的。当前的旋转角度fAngular就是角速度乘以时间fAngularVelocity*time。根据这个角度fAngular可以构造一个选择复数向量rotatevec,根据复数乘法对原顶点坐标向量initpos乘以旋转复数向量rotatevec得到旋转后的顶点坐标向量。

粒子的位移:

half3 localpos=velocity*ftime+0.5f*acceleration*ftime*ftime;

中学物理公式,就不解析了。

粒子的面积:

half fMinSize=parameter4.x;

half fMaxSize=parameter4.y;

half fSize=lerp(fMinSize,fMaxSize,randnum3);

half fWidthRatio=parameter4.y;

half3 objectpos= initpos*half3(fSize,fSize*fWidthRatio,fSize);

half3 billboardpos =mul((half3x3)View, objectpos);

类似地,根据伪随机数randnum3在粒子系统的最小最大面积之间线性插值出该粒子的面积,这个得到的面积fSize同样具有关于j,i,batchstart的确定性。把面积胜于长宽比构造出当前顶点在对象空间的位置向量
initpos*half3(fSize,fSize*fWidthRatio,fSize)。把这个向量作为右矩阵跟当前的视锥矩阵View的3X3部分作乘法得到沿镜头朝着用户眼睛的顶点坐标。这里值得注意的是mul第一个参数是矩阵的3X3部分因为这里是要根据View矩阵的三个向量构造一个微分几何的Frenet标架,用于变换对象空间的顶点向量在世界空间朝着摄像机。这个乘法等价于:

half3 xCamera=normalize(half3(View[0][0],View[1][0],View[2][0]));

half3 yCamera=normalize(half3(View[0][1],View[1][1],View[2][1]));

half3 zCamera=normalize(half3(View[0][2],View[1][2],View[2][2]));

half3 billboardpos = objectpos.x*xCamera+ objectpos.y*yCamera+ objectpos.z*zCamera;

粒子的发生半径:

half fMinRadius=parameter5.x;

half fMaxRadius=parameter5.y;

half fRadius=lerp(fMinRadius,fMaxRadius,randnum3);

half3 offsetpos=(randthree*2.0f-1.0f)*fRadius;

类似地,根据伪随机数randnum3在粒子系统的最小最大发射半径之间线性插值出该粒子的发射半径,这个得到的发射半径fRadius也是关于j,i,batchstart具有确定性的。

顶点的世界空间坐标:

half4 worldpos=half4(billboardpos+localpos+offsetpos+translation,1.0f);

顶点的世界坐标就是把前面算好的特效实例的平移值,该粒子的发射半径,粒子的位移,顶点在对象空间的位置加起来得到。

顶点的纹理坐标:

half textureid=parameter5.w;

half globalu=fmod(textureid,16.0f)/16.0f;

half globalv=floor(textureid/16.0f)/16.0f;

Out.uv=half2(globalu,globalv)+uv0/16.0f;

顶点的纹理坐标就是把原始的纹理坐标变换到合并后的特效贴图上的纹理坐标。先读取当前特效用到的纹理序号textureid,求出这个纹理在合并后的纹理的位置(globalu,gloablv)把这个位置加上原始顶点在自己纹理中的采样位置就得到新的变换后的纹理坐标。

特效的混合模式:

特效的混合模式是指特效与帧缓冲的alpha混合模式,在编辑器里有几种设置: 0暗混合(srcblend= srcalpha, destblend=invsrcalpha),1亮混合(srcblend=srcalpha,destblend=one)。如果混合模式不能统一,那么不同的混合模式就得分组绘制,会产生更多的批次。因此我们想方设法进行合并。首先看看这两种混合的公式:

暗混合:Cf= AsCs+AdCd; Ad=1-As

亮混合:Cf=AsCs+AdCd; Ad=1

Cf是帧缓冲混合完的颜色,Cs是Pixel Shader输出的颜色,As是Pixel Shader输出的alpha值,Cd是帧缓冲混合前的颜色。因此要对这混合模式进行合并就是得同时逐特效指定As和Ad。但是目前的D3D只支持指定这两者的其中一个。由于帧缓冲混合前的颜色Cd是无法修改的,如果我们在Pixel Shader中指定As,我们必须修改destblend这个状态。尽管我们无法修改帧缓冲的颜色,但是我们修改Pixel Shader输出的颜色,也可以用Pixel Shader输出的alpha值As’作为混合中帧缓冲的Alpha值(destblend= srcalpha)同时让Pixel Shader输出的颜色直接乘上原来需要的源混合因子As,即srcblend=1且Cs’=AsCs。

暗混合:Cf= Cs’+As’Cd; As’=1-As, Cs’=AsCs

亮混合:Cf=Cs’+As’Cd; As’=1, Cs’=AsCs

对上面两个式子合并一下有:

Cf=Cs’+As’Cd; As’=1-(1-blend)As

当blend=0暗混合模式时, As’=1-As; 当blend=1亮混合模式时, As’=1

最终我们有下面这段诡异的Shader

result.rgb*=result.a;

result.a=1.0f-(1.0f-blend)*result.a;

return result;

srcblend=one;

destblend=srcalpha;

下面是FX文件中主要的Shaders:

uniform float4 batchdata[238];

float rand(float value)

{

float n=511123;

float b=534;

return fmod(value*n+b,RANDMAX+1.0f);

}

Vertex VS_main(half3 initpos: POSITION0,half3 localindex: BLENDINDICES, half2 uv0: TEXCOORD0)

{

Vertex Out;

int i=localindex.x; //particle system index

int j=localindex.y; //particle index

int k=localindex.z; //vertex index

half4 parameter0=batchdata[i*7+0];

half4 parameter1=batchdata[i*7+1];

half4 parameter2=batchdata[i*7+2];

half4 parameter3=batchdata[i*7+3];

half4 parameter4=batchdata[i*7+4];

half4 parameter5=batchdata[i*7+5];

half4 parameter6=batchdata[i*7+6];

half randnum0=rand(j+i+batchstart)/RANDMAX;

half randnum1=rand(randnum0)/RANDMAX;

half randnum2=rand(randnum1)/RANDMAX;

half randnum3=rand(randnum2)/RANDMAX;

half3 randthree=half3(randnum0,randnum1,randnum2);

half fEmissionInterval=parameter0.w;

half fParticlesPerEmission=parameter1.w;

half fMinLifeSpan=parameter2.w;

half fMaxLifeSpan=parameter3.w;

half fLifeSpan=lerp(fMinLifeSpan,fMaxLifeSpan,randnum3);

half fTotalEmissionInterval=max(MAX_PARTICLE_NUMBER * fEmissionInterval/fParticlesPerEmission,fLifeSpan);

half ftime=saturate(fmod(time-floor(j/fParticlesPerEmission)*fEmissionInterval,fTotalEmissionInterval)/fLifeSpan)*fLifeSpan;

half3 translation=parameter0.xyz;

half3 minvelocity=parameter1.xyz;

half3 maxvelocity=parameter2.xyz;

half3 acceleration=parameter3.xyz;

half3 velocity=lerp(minvelocity,maxvelocity,randthree);

half3 localpos=velocity*ftime+0.5f*acceleration*ftime*ftime;

half fMinAngularVelocity=parameter4.z;

half fMaxAngularVelocity=parameter4.w;

half fAngularVelocity=lerp(fMinAngularVelocity,fMaxAngularVelocity,randnum3);

if (fAngularVelocity>0.01f)

{

half fAngular=fAngularVelocity*time;

half2 rotatevec=half2(cos(fAngular),sin(fAngular));

initpos.xy=half2(initpos.x*rotatevec.x-initpos.y*rotatevec.y,initpos.y*rotatevec.x+initpos.x*rotatevec.y);

}

half fMinSize=parameter4.x;

half fMaxSize=parameter4.y;

half fSize=lerp(fMinSize,fMaxSize,randnum3);

half fWidthRatio=parameter4.y;

half3 objectpos= initpos*half3(fSize,fSize*fWidthRatio,fSize);

half3 billboardpos=mul((half3x3)View,objectpos);

half fMinRadius=parameter5.x;

half fMaxRadius=parameter5.y;

half fRadius=lerp(fMinRadius,fMaxRadius,randnum3);

half3 offsetpos=(randthree*2.0f-1.0f)*fRadius;

half4 worldpos;

worldpos.xyz=billboardpos+localpos+offsetpos+translation;

worldpos.w=1.0f;

Out.worldpos=worldpos;

half4 prjpos=mul(worldpos, ViewProjection); //把顶点从世界空间变换到投影空间

Out.Pos = prjpos;

half textureid=parameter5.w;

half globalu=fmod(textureid,16.0f)/16.0f;

half globalv=floor(textureid/16.0f)/16.0f;

Out.uv=half2(globalu,globalv)+uv0/16.0f;

Out.color=parameter6;

Out.color.a*=1.0f-floor(ftime/fLifeSpan);

Out.blend=parameter5.z;

return Out;

}

half4 PS_main(half4 worldpos:TEXCOORD0,half2 uv:TEXCOORD1,half4 color:TEXCOORD2,half blend:TEXCOORD3) : COLOR0

{

half4 result=tex2D(ParticleSampler,uv)*color;

//远处云雾淡出

half fdistance=distance(worldpos.xz,CameraEye.xz);

half ffog=(fdistance-ffogstart)/(ffogend-ffogstart);

result.a*=1.0f-ffog;

//原纹理的混合比例直接让颜色乘以源的比例

//目的的比例通过destblend=srcalpha来设上去,因为混合模式不一样这个值不一样,用这个来根据混合模式设定帧缓冲的alpha混合比例

result.rgb*=result.a;

result.a=1.0f-(1.0f-blend)*result.a;

return result;

}

technique Render

{

pass Single_Pass

{

AlphaBlendEnable =true;

srcblend=one; //值得注意

destblend=srcalpha; //值得注意

Lighting = false;

cullmode=none;

zenable=true;

zwriteenable=false;

fogenable=false;

VertexShader = compile vs_2_0 VS_main();

PixelShader=compile ps_2_0 PS_main();

}

}

2. 公告板的批量绘制

相对来说,公告板特效的合并要简单些,公告板可以看作是粒子系统中只有一个粒子的粒子系统。

2.1顶点缓冲和索引缓冲的创建填充

首先,我们需要创建足够存储多个粒子系统的所有粒子的顶点缓冲和索引缓冲:

ret=pd3dDevice->CreateVertexBuffer(MAX_SPLITE_BATCH*4*sizeof(PNCT1Vertex),0, D3DFVF_PNCT1Vertex,D3DPOOL_MANAGED, &g_pVB, NULL );

ret=pd3dDevice->CreateIndexBuffer(MAX_SPLITE_BATCH*6*sizeof(int),0,D3DFMT_INDEX32,D3DPOOL_MANAGED,&g_pIB,NULL);

MAX_SPLITE_BATCH是公告板的数量

所有批次的公告板都是共用这个顶点缓冲和索引缓冲,接下来就是填充顶点和索引数据,这里每个顶点的position存的是构成每个粒子的四边形面片的每个顶点在对象空间中的坐标,Shader的语义是POSITION0;每个顶点的索引index的三个分量存的是i公告板的序号,固定的一个0值(公布板内没有再细分的元素了)和顶点在公告板面片4个顶点中的序号,语义是BLENDINDICES;每个顶点的uv存放的是纹理坐标,这里减少了0.01f是为了在合并后的纹理采样时看不到纹理间的裂缝。

vertexcount=0;

for(int i=0;i<MAX_SPLITE_BATCH;i++)

{

pVertices[vertexcount+0].position=float3(-1.0f,-1.0f,0.0f);

pVertices[vertexcount+0].index=int3(i,0,0);

pVertices[vertexcount+0].uv=float2(0.01f,0.99f);

pVertices[vertexcount+1].position=float3(-1.0f,1.0f,0.0f);

pVertices[vertexcount+1].index = int3 (i,0,1);

pVertices[vertexcount+1].uv=float2(0.01f,0.01f);

pVertices[vertexcount+2].position=float3(1.0f,-1.0f,0.0f);

pVertices[vertexcount+2].index = int3 (i,0,2);

pVertices[vertexcount+2].uv=float2(0.99f,0.99f);

pVertices[vertexcount+3].position=float3(1.0f,1.0f,0.0f);

pVertices[vertexcount+3].index = int3 (i,0,3);

pVertices[vertexcount+3].uv=float2(0.99f,0.01f);

vertexcount+=4;

}

2.2公告板批量绘制的Shaders

公告板和粒子系统的主要差别是,公告板的旋转要根据轴来进行,要根据公告板的角速度fAngularVelocity计算出旋转角度fAngular,进而根据旋转轴rotatedir构造一个旋转矩阵matRotate进行旋转。这里的角速度fAngularVelocity就是输入的旋转向量rotatevec的模长,旋转轴rotatedir就是旋转向量rotatevec的单位向量。输入的zPlane是公告板的法线。对zPlane进行两次叉乘,可以构造出一个以输入法线zPlane为z轴,叉乘得到的xPlane和yPlane为x轴和y轴的3×3 Frenet标架(xPlane,yPlane,zPlane),也就是一个坐标基。因此根据输入的法线zPlane可以计算出一个局部坐标objectpos0,根据摄像机的朝向View可以构造出另外一个朝着用户的局部坐标objectpos1。对这两个值作线性插值,当normaltype=0,全局法线模式时,objectpos取objectpos0,当normaltype=1,朝着镜头模式时,objectpos取objectpos1,也可以取浮点的normaltype在两者之间取值。

half3 yPlane=cross(zPlane,half3(1,0,0));

half3 xPlane=cross(yPlane,zPlane);

half3 objectpos0=localpos.x*xPlane+localpos.y*yPlane+localpos.z*zPlane;

half3 objectpos1=mul((half3x3)View,localpos);

half3 objectpos=lerp(objectpos0,objectpos1,normaltype);

Vertex VS_main(half3 initpos: POSITION0,float3 localindex:NORMAL0, half2 uv0: TEXCOORD0)

{

Vertex Out;

int i=localindex.x; //splite system index [0~40]

int k=localindex.z; //vertex index

float4 parameter0=batchdata[i*6+0];

float4 parameter1=batchdata[i*6+1];

float4 parameter2=batchdata[i*6+2];

float4 parameter3=batchdata[i*6+3];

float4 parameter4=batchdata[i*6+4];

float4 parameter5=batchdata[i*6+5];

float randnum0=rand(i+batchstart)/1241.0f;

float randnum1=rand(randnum0)/1241.0f;

float randnum2=rand(randnum1)/1241.0f;

float randnum3=rand(randnum2)/1241.0f;

float fLifeSpan=parameter4.x;

float fLifeStart=parameter4.y;

float fTimeRange=parameter4.z;

float ftime=saturate((fmod(time-fLifeStart,fTimeRange))/fLifeSpan)*fLifeSpan;

half3 translation=parameter0.xyz;

half normaltype=parameter1.w;

half3 zPlane=normalize(parameter1.xyz);

half3 rotatevec=parameter2.xyz;

half fAngularVelocity=length(rotatevec);

half3 rotatedir=normalize(rotatevec);

half fSize=parameter3.x;

half fWidthRatio=parameter3.y;

half3 localpos=initpos*half3(fSize,fSize*fWidthRatio,fSize);

if (fAngularVelocity>0.01f)

{

half fAngular=fAngularVelocity*time;

float4x4 matRotate;

float fCos = cos( fAngular );

float fSin = sin( fAngular );

matRotate[0][0] = ( rotatedir.x * rotatedir.x ) * ( 1.0f - fCos ) + fCos;

matRotate[0][1] = ( rotatedir.x * rotatedir.y ) * ( 1.0f - fCos ) - (rotatedir.z * fSin);

matRotate[0][2] = ( rotatedir.x * rotatedir.z ) * ( 1.0f - fCos ) + (rotatedir.y * fSin);

matRotate[1][0] = ( rotatedir.y * rotatedir.x ) * ( 1.0f - fCos ) + (rotatedir.z * fSin);

matRotate[1][1] = ( rotatedir.y * rotatedir.y ) * ( 1.0f - fCos ) + fCos ;

matRotate[1][2] = ( rotatedir.y * rotatedir.z ) * ( 1.0f - fCos ) - (rotatedir.x * fSin);

matRotate[2][0] = ( rotatedir.z * rotatedir.x ) * ( 1.0f - fCos ) - (rotatedir.y * fSin);

matRotate[2][1] = ( rotatedir.z * rotatedir.y ) * ( 1.0f - fCos ) + (rotatedir.x * fSin);

matRotate[2][2] = ( rotatedir.z * rotatedir.z ) * ( 1.0f - fCos ) + fCos;

matRotate[0][3] = matRotate[1][3] = matRotate[2][3] = matRotate[3][0] = matRotate[3][1] = matRotate[3][2] = 0.0f;

matRotate[3][3] = 1.0f;

localpos=mul(localpos,matRotate);

}

half3 yPlane=cross(zPlane,half3(1,0,0));

half3 xPlane=cross(yPlane,zPlane);

half3 objectpos0=localpos.x*xPlane+localpos.y*yPlane+localpos.z*zPlane;

half3 objectpos1=mul((half3x3)View,localpos);

half3 objectpos=lerp(objectpos0,objectpos1,normaltype);

half4 worldpos;

worldpos.xyz=objectpos+translation;

worldpos.w=1.0f;

Out.worldpos=worldpos;

//把顶点从世界空间变换到投影空间

half4 prjpos=mul(worldpos, ViewProjection);

Out.Pos = prjpos;

half textureid=parameter0.w;

half globalu=fmod(textureid,16.0f)/16.0f;

half globalv=floor(textureid/16.0f)/16.0f;

Out.uv=half2(globalu,globalv)+uv0/16.0f;

Out.color=parameter5;

Out.color.a*=1.0f-floor(ftime/fLifeSpan);

Out.blend=parameter2.w;

return Out;

}

half4 PS_main(half4 worldpos:TEXCOORD0,half2 uv:TEXCOORD1,half4 color:TEXCOORD2,half blend:TEXCOORD3) : COLOR0

{

half4 result=tex2D(SpliteSampler,uv)*color;

//远处云雾淡出

half fdistance=distance(worldpos.xz,CameraEye.xz);

half ffog=(fdistance-ffogstart)/(ffogend-ffogstart);

result.a*=1.0f-ffog;

//原纹理的混合比例直接让颜色乘以源的比例

//目的的比例通过destblend=srcalpha来设上去,因为混合模式不一样这个值不一样,用这个来根据混合模式设定帧缓冲的alpha混合比例

result.rgb*=result.a;

result.a=1.0f-(1.0f-blend)*result.a;

return result;

}

3. 特效纹理的合并

这个就比较简单了,就是把纹理进行一下拼图算法。这个纹理是各种特效粒子系统,公告板,图元轨迹,模型特效等共用的。

posted @ 2011-09-08 18:06 ~哇哇~ 阅读(88) 评论(0) 编辑

棋盘类游戏中的栅格地形渲染

张嘉华 梁成 李桂清

(华南理工大学计算机科学与工程学院 广东 广州 510640)

摘要:本文提出了适合战棋类游戏的三维栅格地形渲染策略,把Catmull-Clark细分曲面思想应用到规则栅格地形,探讨了栅格内顶点和像素的各种属性的计算方法,实现了栅格之间的平衡过度,介绍了在GPU中的实现细节,采用了纹理样式集,几何实例,顶点程序段纹理访问等技术。本文还给出了Vertex Shader和Pixel Shader的伪代码,对于有一定3D图形编程能力的游戏开发人员能够快速再现文本工作。

关键词:战棋类游戏、栅格地形、图形处理器,几何实例

Rendering Grid Terrain for SLG Games

Zhang Jiahua Liang Cheng Li Guiqing

(College of Computer Science and Engineering, South China University Of Technology , Guangzhou Guangdong, 510640, China)

Abstract This paper proposes a 3D grid terrain rendering approach for SLG games, applying the idea of Catmull-Clark Subdivision Surface to regular terrain grids, Methods for calculating various attributes of vertex and pixel in the grid is introduced. This paper also introduces the detail implement on GPU, using technologies such as style texture collection, geometry instancing, vertex texture fetch. This paper gives the pseudocodes of vertex shader and pixel shader. it is helpful for game programmer to implement our work in a short time.

Key words SLG, Tile Terrain, GPU, Geometry Instancing

引言

战棋类游戏(SLG)主要在栅格地形上进行,玩家根据行动顺序指定自方角色沿着栅格行动,进行战斗或冒险。这一类游戏有着庞大的代表性游戏:《超时空英雄传说》系列、《火焰之纹章》、《神奇传说》系列、《天使帝国》系列等。过往都是在二维平面上展现游戏棋盘栅格,随着三维图形技术,特别是现代GPU的迅速发展,已经有很多有代表性的三维SLG游戏,例如:《三国志11》、《信长野望之革新》、《英雄无敌5》等。因此,三维栅格地形的渲染成为了SLG游戏图形模块重要的一部分。

clip_image002

图1:SLG游戏的地图栅格

如图1,游戏场景设计人员往往在地图这些栅格上进行编辑,设置栅格的颜色,纹理样式等以表示这个栅格是雪地,草地还是水体,对于三维SLG游戏还要设置这个栅格的地形高度。

clip_image004

图2:带有渐变和过度的游戏地图栅格

随着游戏的发展,每个栅格若采用单调的颜色来表示往往不能满足需求,因此希望对于场景设计人员来说仍然能够以栅格为单位设置属性,但是栅格与栅格之间能够表现出渐变过渡。如图2,从左到右依次经历水体,沙地,泥地,草地,沼泽地的变化。那么,如何能够既让地形编辑人员能够方便地对每个地形栅格设置属性,同时又能在栅格之间表现出平滑的过度,就是本文研究的重点。

本文的主要贡献包括:1)介绍了一种适合SLG类游戏栅格地形绘制的方法;2)介绍了栅格内顶点和像素的属性位置计算方法,实现了栅格之间的平衡过度;3)介绍了GPU实现细节,采用了纹理集,Geometry Instancing,Vertex Texture Fetch等技术,并给出了Vertex Shader和Pixel Shader的伪代码。对于有一定3D图形编程能力的游戏开发人员能够快速再现文本工作。

接下来的章节将如下组织:第1节先介绍对栅格地形绘制有影响的相关工作;第2节介绍栅格地形绘制的基本框架;第3节介绍了栅格内顶点和像素的属性位置计算方法为第4节GPU实现提供了理论基础;第5节给出了本文方法的运行效果。

1 相关工作

地形的绘制已经有非常多成熟的方案,也有许多适合图形硬件的方案,大致可以分为基于二叉树、四叉树等的LOD方案,适合硬件加速的分块LOD方案、视点依赖的针对GPU的几何裁剪图方案等。

[Duchaineau et al, 1997]提出采用ROAM二元自适应合并分割,通过实时合并和分割三角形来调整地形精度,需要在CPU上不断进行调整。该方法具有灵活的视点依赖的误差评价,能够用户指定三角形数量,具有帧一致性(frame-to-frame coherence)等优点,但该方法并不符合现代GPU Large Batch和少DIP Call的原则。[Röttger et al, 1998]提出对于地形高程绘制采用连续LOD模型来处理,使用四叉树来描述地形高程,对内存需求很少,支持geomorphing,并且能够实现快速处理地形高程的分页(paging of large elevation grids)。

随着2002年后GPU的迅速发展,不再需要在CPU上进行大量严格简化,而需要在CPU简化和GPU批量处理之间取得一个较好的平衡点。[Ulrich, 2002]提出了分块LOD方案。每一个块(chunk)是一个规则格网集,在格网边缘采用垂直裙子方法来填补块与块之间的裂缝。由于每块是一个离散化的整体,有利于GPU批量绘制,该方法有低CPU overhead高渲染吞吐量、能够在aggregate内使用三角网、纹理LOD与地形几何LOD相适应、out-of-core存储、有效平滑的顶点渐变(morphing)、无顶点跃动(poping)和在视点移动时低CPU负荷等优点。但也具有下面这些缺点:数据集必须是静态的,比传统的动态LOD方案需要使用更多的三角形、块内无法进行遮挡剔除。[Larsen ,2003]提出采用硬件优化的LOD地形方案,与Chunk LOD相似基于规则格网四叉树,不同的是对于每个块内部,再内建自适应四叉树,使用Vertex Shader在每个顶点上处理geomorphing。[Ulrich, 2002]提出的分块LOD方法对于栅格地形来说是适用的,因为可以把地形上若干栅格组织成一个块,以块为单位进行视锥剔除和绘制,能够很好地利用GPU,[Larsen ,2003]提出的方法则为我们在Vertex Shader计算顶点属性和位置提供了启发。

信长野望之革新与三国志11的地形引擎和栅格划分类似,都是对地形划分为一个个栅格,如图1,对于棋盘地形中的每个栅格,都有进行LOD处理。这种LOD类似于[Ulrich, 2002]的方法,从测试中只观察出两级LOD。对于图3(a),白色区域选定的栅格由4×4×2个规则三角片构成,当摄像机镜头拉离地表时,LOD级别发生了改变,选定的白色区域的栅格变为图3(b)中的2×2×2个规则三角片构成。

clip_image006

(a)

clip_image008

(b)

图3:三国志11线框渲染,红色边框内的白色线框区域为鼠标选定的某个栅格

2 基本框架

整个地形都是按栅格进行划分的。图4是一个栅格(grid)的定义,栅格的纹理样式和颜色都定义在中心采样点上,对于输入256×256的原始地形,就有256×256个栅格。每个栅格由四个子区域(sub rect)组成,每个子区域由4个顶点构成。读者肯定会问一个问题,为何不直接对每个栅格用一个顶点表示其中心点呢?这是因为不同栅格的各种属性之间需要过渡,并且边界上的顶点属性并不是都刚好满足周围4个采样点的双线性插值关系,需要在栅格边界上放置经过计算的顶点以保证属性过渡正确,并且使栅格内非边界区域能展现该栅格设置的采样点属性。

因此,每个子区域2×2个顶点,共2×2×4=16个顶点。对于一个256×256原始采样点的图来说,那么就需要256×256×16个顶点。那么读者又会问,为何不是一个栅格只要周围8个边界上的顶点和一个中心顶点呢?这是由于每个子区域都是由它周围4个采样点按照一定比例混合过渡的。如图2,8号顶点和2号顶点则被看作在不同的子区域,对它有权重影响的周围4个采样点是不一样的,因此两个顶点的4层纹理坐标也有差别,如果不采用Shader是难以实现每个栅格只用9个顶点。

clip_image009

图4:1个栅格(Tile)的定义

地图设计编辑人员往往希望只需要对每个栅格独立设置各种属性,包括纹理样式和颜色等,这和细分曲面很类似,往往希望通过修改低分辨率的控制网格但产生高分辨率光滑的网格。因此,对某个栅格(i,j)设置属性就等价于为中心采样点设置属性。栅格中心采样点的某种属性定义为a(i,j),栅格中每个顶点也拥有自己的属性,如图4中构成该栅格的16个顶点的对应属性为{ak(i,j)|k=0,1,..,15}。这里需要说明的是,v3v6v9v12四个顶点从二维投影来看与采样点是重合的,但是从三维空间上来看,最终计算得到的位置并不一定与采样点重合的,这取决于计算模板,对于Catmull-Clark模式这4个顶点将逼近通过采样点所构造的极限光滑曲面。

考虑到用户编辑和选择的最小单位是栅格,那么为了表现该栅格,最小需要16个顶点来表现一个栅格。因此,在对地形进行LOD处理时,不能够通过合并栅格来减少顶点数量,至少也要维持每个栅格4×4=16个顶点。有鉴于此,本文采用了[Ulrich, 2002]的方案分块来进行处理,每4×4=16个栅格组成一个块(chunk),每个块内LOD时最低分辨率约束不能低于(4×4)×(4×4)=256个顶点。LOD采用4个级别,最高级别时每个块内有128×128=16384个顶点。由于栅格的子区域四层纹理是不同的,因此,不能对栅格与栅格边界上的顶点进行合并减少顶点。在每帧,视锥剔除和LOD级别计算等都是以块为单位进行。

对于地形范围比较少的场景,也就是采样点少于等于256×256的场景,可以预先构建好整个地图所有的块,并生成每个块的所有LOD级别,但是这样存在较多空间冗余,因为每帧绘制中,大部分的块都采用较低的LOD级别,而采用较高的级别只有1至2个视点正在关注的感兴趣的块。如果,每个块都生产好各个LOD级别,一个块所有LOD级别共需要256+1024+4096+16384=21760个顶点,整个256×256个栅格的场景共有64×64=4096个块,那么整个场景生成时就有21760×4096=89128960个顶点,而实际每帧绘制的地形也就约2×16384+6×4096+16×1024+64×256=90112个顶点(2,6,16,64为平均每周视锥剔除后场景中各个LOD级别块的数量),也就是绘制的顶点约为总顶点数量的0.1%。为了解决这个问题,已经有许多Out-Of-Core的外存地形以及内存分页映射的地形数据加载调度算法,这些算法对于海量数据(例如TB级的数字地球的金字塔影像数据库)来说是合适的,但是我们整个场景只有256×256个栅格,也就是65536个采样点,大部分高分辨率LOD级别的顶点数据都是插值得到的,并不是真的存在那么多高分辨率采样点,之所以存在那么多顶点,主要一个原因就是对应一个采样点的栅格就产生了至少16个顶点。

由于整个场景的栅格一般只有256×256数量,没有必要运行时进行内外存数据交换,但是我们又希望当镜头靠近时能插值出高分辨率的顶点。因此,是否有一种无需生成好具体顶点,能够延迟具体顶点位置和属性计算到绘制时再在GPU处理的方法呢?

Geometry Instancing技术和Vertex Texture Fetch技术使得这样的想法成为了现实。在CPU中,只要为其中一个块生成好各个LOD级别的顶点缓冲,这里记这些顶点缓冲为VBββ为LOD级别,取0~3。顶点缓冲VBβ中各个顶点的位置只需要填上相对于块内的位置,绘制时在Vertex Shader再计算具体的位置。有了这样的基础后,绘制时对于每个块根据自己当前帧的LOD级别选择对应的VBβ来绘制。如果采用Geometry Instancing方式绘制,为同一个LOD的各个块设置一个实例数组放到实例缓冲,通过SetStreamSourceFreq设置后绘制;对于不支持Geometry Instancing的GPU,可以对于同一个LOD级别的各个块分别调用DIP call。这使得同处于β这个LOD级别的各个块都可以共享一个顶点缓冲VBβ。这大幅度减低了顶点数量,在CPU分配顶点缓冲时只需要21760个顶点。

为了能够在绘制时再计算出具体的顶点位置和属性,因此需要用到Shader Model 3.0的Vertex Texture Fetch技术。首先,我们把场景地图中的256×256个采样点的位置、颜色和选用的纹理样式分别存放到3张纹理TPTCTSTP中的一个像素TP(i,j)对应采样点的位置,记为p(i,j),TC中一个像素PC(i,j)对应采样点的颜色,记为c(i,j),TS中一个像素TS(i,j)对应采样点选用的纹理样式t(i,j)。在GPU的Vertex Shader根据输入的每个待插值顶点在块内的编号得到顶点在栅格内的编号k,通过tex2DLOD指令从这3张纹理读出临近4个采样点的数据,加权得到这个插值顶点的数据。具体顶点的属性和位置计算在第3节介绍。

3. 属性和位置的插值计算

第2节介绍了栅格地形绘制的基本思路,那么如何计算栅格内的16个顶点和栅格覆盖的各个像素的属性和位置呢?下面3.1节将介绍顶点除位置外各种属性包括纹理、颜色的计算,3.2节进一步介绍像素属性的计算,3.3节介绍顶点的位置计算。

3.1. 顶点属性计算

clip_image010

图5:某个栅格与其邻接栅格示意图

为了实现栅格间的各种属性(纹理,颜色等)的融合,先来考察最为简单的双线性混合。下面推导栅格上每个顶点的属性权重比例。对于浅黄色子区域(8-9-10-11),参见图5,实质上是采样点A-B-D-E所构成的区域的右下角,因此顶点9的某个属性如下计算:a9=0.25(aA+aB+aD+aE),wi为四维向量,分别代表影响该子区域的四种类型纹理的权重,其分量顺序对应每个子区域的纹理样式编码,均为左下,左上,右下,右上,例如w0=(0.25,0.25,0.25,0.25),类似可以得到下面关系:

clip_image012

clip_image014

clip_image016

clip_image018

(1)

3.2. 像素属性计算

不但要根据采样点得到顶点颜色,更进一步需要根据采样点得到栅格内某个像素的颜色。虽然现代的GPU都已经能够在硬件实现双线性插值,但是为了后文讨论方便,下面还是先对每个像素的属性计算先作进一步推导。

nclip_image019

图6:逐像素纹理混合

假设栅格坐标如图6,栅格内顶点像素范围取值从-0.5至0.5,取u1=floor(u),u2=ceil(u),v1=floor(v),v2=ceil(v),那么对于栅格内任意一个像素,影响其的四个栅格(采样点)分别为(i+u1,j+v1),(i+u2,j+v1),(i+u1,j+v2),(i+u2,j+v2),对于如图4,假设像素处于右下角子区域,那么则u1=0,u2=1,v1=-1,v2=0,影响它的四个栅格,也就是提供它融合权重的四个采样点分别为(i+0,j-1),(i+1,j-1),(i+0,j+0),(i+1,j+0)。接着计算混合比例u’=fmod(u,1.0),v’=fmod(v,1.0),即对1.0求浮点模,那么栅格内任意点(uv)的属性可以通过公式(2)双线性插值计算得到:

clip_image021(2)

3.3 顶点位置计算

考虑到每个栅格的顶点位置是定义在栅格的中心采样点上,为了使每个栅格周围的16个顶点位置能够更加平滑,因此可以考虑采用细分曲面的Catmull-Clark细分模式来计算16个顶位置。

clip_image022

(a) V顶点模板 (b) E顶点模板 (c) F顶点模板

图7:Catmull-Clark细分模板

根据图7中Catmull-Clark细分模式的三种模板,从图5可以看出,0,5,10,15号顶点属于F顶点,1,2,4,7,8,11,13,14号顶点属于E顶点,3,6,9,12号顶点属于V顶点,因此有下面的式子;

clip_image024

(3)

应用了Catmull-Clark模式计算栅格的16个顶点坐标后,整个地形能显得更加平滑,但是Catmull-Clark模式会改变顶点的位置以逼近极限的光滑曲面,因此考虑一种能够保持顶点位置的模板。这种模板需要更大范围的领域模板顶点去计算一个新的顶点位置

Dafdasfasdf

(4)

4.实现细节

在具体实现时,首先在CPU生成好各个LOD级别的顶点缓冲VBβ。在绘制时为每个块进行视锥剔除,剔除后剩下需要绘制的块选择合适的LOD级别调用绘制函数。在GPU每帧处理中,Vertex Shader根据原始输入的采样点的数据,计算栅格地形顶点的具体位置、颜色、4层纹理的纹理坐标以及权重,然后GPU对顶点颜色,权重以及纹理坐标进行双线性插值产生Pixel Shader每个像素的输入。在Pixel Shader中,对于每个输入像素,根据插值得到的4层纹理坐标访问纹理得到4层纹理的颜色,根据权重对各层纹理颜色进行混合,再和输入的像素颜色混合,得到最终的Pixel Shader输出的颜色。

4.1. 纹理样式集

为了减少纹理上下文的切换,可以考虑把各种样式纹理组合到一张大的纹理集S上,本文采用2048×2048的大纹理作为纹理集S来存放16×16=256张样式纹理,图8是一张16×16组合的纹理集。每个样式纹理大小为64×64个像素。

clip_image026

图8:16×16纹理集组合

生成了纹理集后纹理坐标需要从样式纹理坐标到纹理集坐标的转换:

clip_image028 (5)

其中u’,v’为转换后纹理集的坐标,uv为样式纹理的坐标,pq为样式纹理在大的纹理集S中的横纵序号。

4.2. CPU预处理

接下来需要在CPU中生成好各个LOD级别的顶点缓冲VBβ,那么需要先定义顶点缓冲中顶点的结构。由于顶点并不再存放实际顶点位置,每个顶点只需要3个纹理通道,一个存放顶点所处栅格在块内的编码,一个存放顶点在栅格内的编码k,一个存放顶点在栅格内的归一化坐标:

struct GridVertex

{

public:

float2 mn;//顶点所处栅格在块内序号,TexCoord0

float k; //顶点在栅格内序号,TexCoord1

float2 uv;//顶点在栅格内归一化坐标,TexCoord2

};

顶点在栅格内的归一化坐标实际上都可以在Vertex Shader结合常量表设置的每个绘制块最左下角栅格序号推导得到,但是为了减少Vertex Shader运算,因此直接在CPU计算好了。

在CPU中,每个栅格的中心点属性和位置存放在在纹理TPTCTS当中,那么对每个栅格的属性和位置修改可以看作对纹理TPTCTS的某个像素的再渲染,通过渲染到纹理(RTT)来实现。这使得实时编辑和修改栅格成为了可能,并且在修改完成后无需对顶点缓冲锁定和解锁,无需重更改顶点缓冲中顶点属性和位置。

4.3 Vertex Shader

4.2节中介绍的顶点的位置和属性计算需要在Vertex Shader进行,沿用第2节中对p(i,j),c(i,j),t(i,j)的定义,栅格内每个顶点vk可以根据公式(2)看作u,v落在栅格边界上的顶点处理,在Vertex Shader如下计算:

uniform int left_tile;

uniform int bottom_tile;

VertexOut VS_main(VertexIn In)

{

float u=In.uv.x;

float v=In.uv.y;

u1=floor(u);

u2=ceil(u);

v1=floor(v);

v2=ceil(v);

float i=In.mn.x+ left_tile;

float j=In.mn.y+ bottom_tile;

VertexOut Out;

Out.uv0=f(t(i+u1,j+v1));

Out.uv1=f(t(i+u2,j+v1));

Out.uv2=f(t(i+u1,j+v2));

Out.uv3=f(t(i+u2,j+v2));

c(u,v)=CalcByEquation(2);

Out.c0= c(u,v);

w(u,v)=CalcByEquation(2);// 四层纹理权重

Out.w= w(u,v);

p(u,v)=CalcByEquation(3);

float4 worldpos=p(u,v);

Out.pos=mul(worldpos,ViewProjection);

return Out;

}

算法1:Vertex Shader的伪代码

这段Vertex Shader首先根据输入的顶点在栅格内的归一化坐标uv以及在这个块绘制前通过Shader常量设置的这个块左下角栅格序号left_tile和bottom_tile计算得到四个采样点坐标的横纵分量u1,v1,u2,v2,接着得到这4个采样点坐标(i+u1,j+v1),(i+u2,j+v1),(i+u1,j+v2),(i+u2,j+v2),接着利用Vertex Texture Fetch技术支持的tex2DLOD指令从纹理G读取影响当前顶点的4个栅格采样点数据,然后根据公式(2)和公式(3)计算出这个顶点属性和位置输出。

4.4 Piel Shader

对于Pixel Shader输入的每个像素的各种属性,其中像素颜色可以由GPU根据4个顶点颜色进行双线性插值得到,在CPU计算好每个顶点的颜色即可,纹理的双线性插值由于需要根据不同纹理坐标采样了再进行插值,因此可以在CPU根据顶点在栅格内坐标计算后四个层次的纹理坐标,在Piel Shader输入前由GPU双线性插值得到每个像素分别对应4层纹理的坐标,对四个纹理层次进行纹理采样(Sample)得到4层纹理的颜色,然后根据权重得到最终的纹理颜色。

在Piel Shader里,对于每个子区域的每个像素,GPU会根据四个角点的权重向量和颜色向量进行双线性插值得到具体每个像素的权重向量w(u,v)和c0(u,v)作为Piel Shader的输入,在Piel Shader只需要对四种纹理分别根据w(u,v)的四个分量为权重进行tex2D纹理采样,得到该像素的纹理颜色:

float4 PS_main(PielIn In) : COLOR0

{

float4 tc[4];

tc[0]=tex2D(S,In.uv0);

tc[1]=tex2D(S,In.uv1);

tc[2]=tex2D(S,In.uv2);

tc[3]=tex2D(S,In.uv3);

float4 tc=In.w[0]*tc[0]+In.w[1]*tc[1]+

In.w[2]*tc[2]+In.w[3]*tc[3];

return tc+In.c0-0.5f;

}

算法2:Piel Shader的伪代码

5 结果与讨论

我们在配置有NV8400m GPU,2G内存的计算机上进行了测试,每秒能够保持100帧以上的FPS值。图9是我们开发的一个SLG游戏《武林群侠传》的地图编辑器,绿色区域是正在选择编辑的3个栅格,用户可以对选择范围内的栅格进行各种地形操作:诸如抬升高程,降低高程,铲平到水平面;设置该范围栅格的颜色;设置该范围栅格用的纹理样式。用户可以改变选择的栅格范围大小(绿色区域大小),有1X,3X,5X和自定义范围4种选择。

对于用户上述对绿色编辑区域的任何修改,实时通过渲染到纹理(RTT),更新纹理TPTCTS对应区域像素,即使更新整张256×256纹理,即重绘整个栅格采样点数据纹理也不到1ms时间,用户完全察觉不到,能够实时响应用户的操作。

clip_image030

图9:栅格地形渲染效果

与英雄无敌5附带的地图编辑器等相比,本文应用了Geometry Instancing和Vertex Texture Fetch技术,任何的编辑修改操作,都直接通过渲染到纹理更新显存上的数据,无需任何对顶点缓冲和索引缓冲的锁定和解锁,避免了对于编辑范围较大时的锁定卡帧现象。同时无需预先分配大量的显存存放各个级别LOD的数据。另一方面,本文提出的方法能够产生光滑的地形曲面,在本质上与细分曲面很类似,细分曲面是对原始低分辨率的控制网格进行修改,产生光滑的极限曲面,本文方法是对低分辨率的规则格网上的采样点数据进行修改,产生平滑的地形。不同的是,本文方法还要考虑栅格所具有的纹理颜色属性,并要保证这些属性能够平滑过渡。

参考文献

1. Mark Duchaineau, Murray Wolinsky, David E. Sigeti, Mark C. Miller, Charles Aldrich, Mark B. Mineev-Weinstein. Real-time Optimally Adapting Meshes. IEEE Visualization 1997 , 81~88.

2. Stephan Röttger, W. Heidrich, Ph. Slusallek, H.-P. Seidel. Real-Time Generation of Continuous Levels of Detail for Height Fields. Procceeding of WSCG 1998, 315~322.

3. Thatcher Ulrich. Chunked LOD: Rendering Massive Terrains using Chunked Level of Detail Control,Course Notes of ACM SIGGRAPH 2002.

4. Bent Dalgaard Larsen, Niels Jørgen Christensen. Real-time Terrain Rendering using Smooth Hardware Optimized Level of Detail. Journal of WSCG 2003, 11(2):282~289.

5. Frank Losasso, Hugues Hoppe, Geometry clipmaps: Terrain rendering using nested regular grids,Proceedings of ACM SIGGRAPH 2004, 23(3): 769~776.

6. Arul Asirvatham,Hugues Hoppe,Terrain rendering using GPU-based geometry clipmaps,GPU Gems2 Chapter 2, 27~44.

7. Nick Brettell. Terrain Rendering Using Geometry Clipmaps. Dissertation, Canterbury University 2005.

8. Malte Clasen, Hans-Christian Hege. Terrain Rendering using Spherical Clipmaps. Proceedings of EuroVis06(Joint Eurographics - IEEE VGTC Symposium on Visualization 2006).

posted @ 2011-09-08 18:04 ~哇哇~ 阅读(147) 评论(0) 编辑

三国志11的一点分析

张嘉华 newzjh@126.com QQ:188318005

由于很多朋友问到我三国志11的渲染问题,今天只好自己用GA加Fraps,还有RPGViewer分析了一下,并把分析弄了段录像,三国志11与信长野望革新两个游戏用了类似的水墨地形渲染方法,参考视频,可以简单分析到几点

1、 LOD

对于战棋地图环境中的每个格子,都有LOD,那么这种LOD应该是类似Tiled Chunk的方法,而不是基于四叉树自适应方法,看上去只看出两级LOD

clip_image002

上图的那个战棋格子chunk由4*4*2三角形构成

clip_image004

下图那个chunk三角形数量减半

2、 天空背景

三国志11的天空背景很特别,用了一种圆柱状天空围绕,主要可能是因为限制了镜头不能提高,因此围一圈就够了,下图是渲染时从线框看出来围着形式的天空,再下图是

clip_image006

用RPGViewer解出了三国志11里面天空对应的背景图8张

clip_image008 clip_image010

clip_image012 clip_image014

clip_image016 clip_image018

clip_image020 clip_image022

把这几张图片绕着场景画一圈就是天空背景了。

3\水墨勾边

clip_image024

clip_image026

三国志的水墨勾边主要是看三角形的法线,用摄像机的视向量与地形三角形的法向量点积,然后把颜色减去这个点积值放大某个系数后的结果就是最终的勾边效果。下面是我做的游戏的一段模拟勾边的效果。

float3 viewdir=normalize(CameraLookat.xyz-CameraEye.xyz);

float dotvalue=dot(In.normal,viewdir);

float edgecolor=max(dotvalue,0.0f)*10.0f;

posted @ 2011-09-08 18:01 ~哇哇~ 阅读(71) 评论(0) 编辑

数字地球大气散射的GPU实现

张嘉华 梁成 李桂清

(华南理工大学计算机科学与工程学院 广东 广州 510640)

摘要:本文综合介绍了[Nishita et al 1993]提出的大气散射的物理模型以及[O’Neil 2004, O’Neil 2005]提出的GPU精确大气散射方法,并在此基础上逐步解析了在GPU上实现大气散射效果的原理。在具体实现上,天幕网格建模方面采用了三角片大小一致的构造方式,在GPU上通过若干点的采样实现散射的外积分,通过经验的多项式函数逼近Optical Depth值,代替查找表计算散射的内积分,并给出了在Vertex Shader和Pixel Shader的实现细节。本文将是一篇针对工程人员在实现数字地球的大气散射时好的技术导向文献。

关键词:数字地球、大气散射、GPU、实现细节、顶点程序段、像素程序段

GPU Atmospheric Scattering Of Digital Earth

Zhang Jiahua Liang Cheng Li Guiqing

(College of Computer Science and Engineering, South China University Of Technology , Guangzhou Guangdong, 510640, China)

Abstract This paper synthetically introduces the atmospheric scattering model proposed by [Nishita et al 1993] and accurate GPU atmospheric scattering approaches proposed by [O’Neil 2004, O’Neil 2005]. This paper also explains the theory of atmospheric scattering effect based on GPU step by step. On the implementation, outer atmosphere mesh is modeled by the triangles in the same size. We calculate out-scattering integral by suming at sample points and use experiential multinomial function to approximate Optical Depth value in order to replace lookup table for calculating in-scattering integral. We also give a detail implement about vertex shader and pixel shader. This paper may be a good technologic reference for engineers to implenment atmospheric scattering of digital earth.

Key words Digital Earth, Atmospheric Scattering, GPU, Detail Implement, Vertex Shader, Pixel Shader

引言

大气散射是构成天空颜色的主要因素,也是距离眼睛点很远的对象淡出视野的主要原因。真实感的大气散射效果在场景漫游、飞行模拟等方面都有重要的应用。完全真实地按照大气散射模型进行计算需要很大的计算量,难以实时实现,因此,可以在简化的物理模型上在GPU上加以实时实现。现有的大气散射方法大部分假设大气密度是常数,这可能产生不够真实的效果。

本文实现的程序能够产生视觉上比较逼真的散射效果,并且计算量较少,能够在GPU的Vertex Shader和Pixel Shader无需Shader Model 3.0的情况下实现。本文的主要贡献是详细地解析了[Nishita et al 1993]大气散射模型原理,并探讨了[O’Neil 2004, O’Neil 2005]的GPU实现细节,给工程人员在GPU上实现数字地球大气散射以清晰的思路和细节。

第1节介绍大气散射的相关工作,第2节介绍介绍[Nishita et al 1993]提出的大气散射物理模型及其延伸,第3节介绍了一种三角片大小相等的天幕建模方法,第4节介绍我们在[O’Neil 2004, O’Neil 2005]基础上大气散射效果的GPU细节实现,第5节给出实现的效果。

1 相关工作

大气散射效果的真实感程度与大气散射的物理模型有很大关系,[Nishita et al 1993]提出的大气散射模型认为大气中包含分子和悬浮物质,阳光穿透大气到达视点过程中会产生散射,到达视点的光线密度是光线上所有微粒散射的光线的积分,大气中散射的光线同样能够到达视点,大气的颜色由最终到达视点的光线决定。[Nishita et al 1993]还介绍了从宇宙外和地球内两个不同的角度绘制大气散射效果,随后[Nishita et al 1996,4]介绍了一种快速计算单重大气散射的天空光亮强度分布的方法,并介绍了一种有效计算考虑大气多重散射的天空颜色的方法。[Nishita et al 1996,5s]在[Nishita et al 1993]工作的基础上探讨了大气的各向异性(anisotropic)多重散射,

[O’Neil, 2004]对[Nishita et al 1993]工作进行了简化,在GPU上通过循环采样计算散射的外积分,通过构建一个预生成的查找表来计算散射内积分Optical Depth值。[O’Neil, 2005]用多项式函数逼近来代替查找表计算Optical Depth值,使得每个顶点的散射能够在GPU的Vertex Shader上实现,但是上述方法都没有详细地介绍和解析其实现原理和推导过程,对于普通工程人员实现该方法仍存在一定难度。

2 大气散射的物理模型

clip_image002

图1:与大气相交的光束强度计算

图1是一次散射的示意图。在P点,入射光线散射到视向路径上(单重散射)。经过一次或多次散射的光线在P点同样朝眼睛点E方向散射。视向和光线方向间夹角θ叫做散射角,该角度是后文提到的散射相位函数的变量。到达眼睛点E的光线总量是沿着整个视向路径上所有散射效果的综合。从P点到达E点前散射光线会收到大气分子的吸收和削弱。

假设大气层密度是连续变化的,大气的颜色主要受太阳光光谱、大气微粒的散射和吸收作用、地表的反射光和太阳与视点位置关系影响。太阳光进入大气层后将会被大气分子和微粒和臭氧层散射和吸收。散射的特征依赖于大气微粒的大小,当散射粒子和波长相比足够少(r<λ/10),例如大气分子等细小粒子,引起的散射叫做Rayleigh散射,散射强度在各个方向是一致的;由悬浮微粒等大粒子(r>λ/10)引起的散射叫做Mie散射,散射强度在各个方向的分布变得复杂,在前向方向上有较多散射能量,两种散射都会削弱光线强度。

如图1,在P点发生散射,那么散射后的光强度IPo有:

clip_image004 (1)

其中IPiP点散射前的入射光强度,hP点离大气层内边界的高度,λP点处光波长,θ是散射角度,G(h, λ, θ)是对Rayleigh散射和Mie散射进行混合的调和函数。为了更好地理解散射物理模型,2.1节和2.2节先介绍Rayleigh散射和Mie散射的物理模型,调和函数G(h, λ, θ)将在2.4节再介绍。

2.1 Rayleigh散射

Rayleigh散射主要由空气中的分子(干净的大气)产生,仅与可见光的散射相关。散射相位系数ωR(θ)(volume angular scattering coefficient),描述了指定波长λ下,在给定方向θ上散射光线的数量,对于Rayleigh散射可以通过公式(2)计算[Irwin 1996]:

clip_image006 (2)

其中θ是视向和太阳光方向的夹角,n是大气折射率,N是大气分子密度,λ是入射光波长,FR(θ)是单位化相位函数(normalised phase function),描述散射的方向特征:

clip_image008 (3)

Rayleigh 散射的重要性质是1/λ4依赖于波长,短波长光线比长波长的光线更容易散射。短波光线在经过大气时收到非常强烈的削弱,长波光线收到很小影响。这就是为什么白天的时候天空呈现蓝色的原因,而在日落和日出的时候,通过的光线增加,天空的颜色由于短波增多而变红。

散射削弱系数βR(θ)(单位长度的削弱比例)是对公式(2)中ωR(θ)所有角度进行积分导出的,描述了指定波长λ下散射光线的数量:

clip_image010 (4)

βR(λ)描述了散射对原入射光线数量的总的削弱程度。

clip_image012

图2:Rayleigh散射相位函数FR(θ),进入视线方向的散射光的强度作为散射角度的函数

大气密度比用ρR (h)表示,高度h距离大气层内边界的高度,ρR (h)随h变化而变化,可以根据下面公式计算:

clip_image014 (5)

其中ρ0取1.00006,H0缩放高度常取值为7994m或8300m。

2.2 Mie散射

Mie散射是一种可以由任意大小粒子产生的散射,从这个角度来说, Rayleigh 散射是Mie散射的一个子集,把Mie散射应用到小粒子上会产生和Rayleigh散射一样的结果。Mie散射相位系数ωM(θ)可以表达如下;

clip_image016 (6)

其中c是浓度因子随着混乱T而改变,按如下计算[Preetham et al. 1999]:.

clip_image018 (7)

FM(θ,g)是Mie散射相位函数,随着散射粒子大小的改变而改变,可以通过一个著名的相位函数——Henyey-Greenstein函数来近似表达:

clip_image020(8)

其中g是一个不对称因子:

clip_image022(9)

如果g=0,那么公式(8)等价于Rayleigh散射相位公式(2),u由大气条件决定,取值范围由0.7-0.85,因此g取值范围为-0.75至-0.999,g值不能取1或者-1,否则会令公式(8)为0。

clip_image024

g=0.20

clip_image026

g=0.55

图3:Mie散射相位函数FM(θ,g)

散射削弱系数βM(λ):

clip_image028 (10)

其中KM随着波长改变而改变。图4给出了Mie散射函数的图像。

clip_image030

(a) 小粒子Rayleigh散射

clip_image032

(b) 大粒子Mie散射

图4:Mie散射角散射函数

与大气分子的密度分布类似,悬浮微粒的密度分布也与高度呈指数关系,但系数与大气分子不同,H0取1.2Km。

2.3 光学深度(Optical Depth)

在大气光学经常用到光学深度(Optical Depth)这个术语,Optical Depth经常用于光学路径通过指数形式表达散射削弱程度,可以通过散射削弱系数的积分形式表达。

clip_image034 (11)

其中β(λ)= βR(λ)+ βM(λ),是Mie散射削弱系数和Rayleigh散射削弱系数之和,ρ (h)随高度h改变,而h与路径l上的某段元路径dl的位置P有关,是变元。这里Optical Deptht表示波长为λ,通过距离为l的光线受大气分子的削弱程度,可以认为是一个权重因子描述大气中沿着光束路径上微粒多少的量,它反映了微粒对光线的削弱程度。Optical Depth能够用于直接计算经过一段路径上的光线削弱后光亮量:

clip_image036 (12)

根据上述推导,如图1所示,到达视点E的光线是经过沿着BE路径的大气散射和吸收的余下部分,到达P点的光线已经被PC之间的大气分子和微粒削弱了,从P点散射的光线在到达E前同样被大气分子和微粒给削弱了。

2.4 视点处散射的总光强度

对于一束光线,随着视点位置不同,到达视点的光强度也不同。到达E的光可以看作是由于大气分子沿着光束和大气的交线AB散射和吸收的剩余部分。大气层C点处的光强度IC(λ)=IS(λ),IS(λ)是大气层顶部入射光的太阳辐射强度,接下来考察在AB上的某一个点P点,P点散射前沿CP方向的入射光强度IPi(λ)可以通过把IC乘以CP段削弱程度量计算得到。通过对公式(11) Optical Depth计算公式中设置CP为积分区间计算得到IPi(λ):

clip_image038 (13)

其中t(CP, λ)是从大气层顶部到P点的Optical Deptht(CP, λ)的计算公式如下:

clip_image040 (14)

其中,s是积分变量。在P点发生散射,应用公式(1)计算散射后的P点沿PE方向的光强度IPo,公式(1)中的调和函数G(h, λ, θ)如下计算:

clip_image042 (15)

既然光线从P到达E点过程中也受到削弱,那么E点的光强度IE可以通过把P点散射后的光强度IPo乘以PA区间光学削弱程度量计算得到,PA区间光学削弱程度量与公式(14)类似:

clip_image044

(16)

由于地球到太阳的距离可以认为是无限的,因此太阳光可以认为是平衡光,沿着AB每个点的散射角度可以认为是常数。IE反映的是太阳光通过CP在P点散射后沿PA到达E后的光强度,但是太阳光除了在P点散射外,在AB上的任何一点也存在散射,因此,E点的总的光强度IEtotal需要通过对由于大气分子散射光在AB段上发射了散射到达E的所有IE进行积分计算得到:

clip_image046

(17)

3天幕的几何建模

第2节介绍了大气散射的物理模型,大气散射的绘制实体并不一定是大气层,它可能是地表或者云彩,月亮等其他对象,大气散射效果同样作用在这些对象的绘制中,本文只介绍天幕为绘制对象的情况,其它情况可以参考[O’Neil,2004]。常见的建模方法是构造一个用极坐标表示的半径很大的天幕曲面网格来表示天幕。为了简单起见,大气的厚度忽略不计,只对几何建模好的大气天幕上的每一顶点正确计算屏幕投影位置,每一个象素计算散射后的颜色。因此,我们可以把大气层表示为一层与地球同心球面天幕,其离地高度可以取大气层的平均海波高度。

clip_image048

图5:极地地区的经纬度构建的天幕网格

根据绘制的精度不同,通常对这层天幕在极坐标下根据经纬度适当均分构成一个球面网格,采用D3D中的Vertex Buffer和Index Buffer构建(OpenGL中可以采用gluSphere()函数)。如图5,可以相隔若干经度和维度创建一个栅格,但这会导致在极点附近极地区域三角片比较密集,而在赤道地区的三角片很大,因此如果要赤道区域都能很好地用密集的三角片表示,那么需要在极点附近使用非常多的三角片。

clip_image050

图6:极地地区大小相等三角片构建的天幕网格

为了解决上述问题,因此希望有一种三角片大小相近的组织方法,保持维度带的宽度,并且用相同大小的三角片组织每个维度带。首先把经线带划分为相等大小的片,然后在这些顶点位置创建三角形。有时,这会导致额外的三角片添加和删除依赖于条形带相对于前一个带是展开还是收缩。算法使用一系列条件去决定是否插入一个额外的三角片。

如图1,我们看到的最终大气效果是以E点作为视点观察整个天幕,对于天幕上的每一个顶点,如AB等将在Vertex Shader计算出其投影在屏幕后的位置,并在Pixel Shdaer根据上述大气散射的物理模型计算出其最终颜色。

4 GPU实现细节

如图7,从视点E到大气层天幕顶点B的光通过大气层,与大气层存在交线AB,那么A点定义为光束通过大气层的起始点,B点定义为光束通过大气层的结束点。假设视点和绘制的网格实体顶点均在大气层外的宇宙空间,那么还需要对光束和大气层进行求交,求出A点和B点。根据上文提到,天幕是以大气层外层作为绘制的几何网格,因此,B点通常就是天幕的顶点,也就是Vertex Shader中输入的顶点,只需要计算A点。

4.1 外积分计算

第2节介绍的[Nishita et al 1993]提出的散射模型里存在大量的积分,可以采用梯形法来计算积分,把积分形式转化为循环采样计算后求和,对于某一路径上的积分,采样越多得到的近似积分计算结果约精确,但需要更大的计算量。如图7,对于公式(17)的外积分,为了计算这个外积分值,在从ABP1至P5五个点上进行采样计算,首先计算出五个采样点的坐标,可以根据线段AB线性插值得到。考虑到GPU需要Shader Model 3.0以上才支持动态循环上限,因此只采用了P1—P5五个固定点来采样计算积分。公式(5)中的h可以用采样点坐标转换为极坐标的后的半径值R来计算。

clip_image052

图7:大气散射示意

4.2 内积分计算

即使对外积分进行采样计算后,上述的散射模型的计算量仍然是非常大的,主要是公式(11)的Optical Depth的内积分计算,在GPU实现将会非常慢。为了计算沿着一条光束路径的近似Optical Depth,可以简单地把该光线路径分解为一系列更小的“采样”路径段(元路径),然后循环遍历这些元路径,求每个元路径β(λ)ρ (h) dl的总和,h取每段元路径dl的中心点Pavg的大气高度havg。光线分解得越小,Optical Depth的积分越精确。

对于公式(17),若内积分也5次采样,外积分5次采样,那么共需要在Vertex Shader中进行5×(5+5)次采样每个顶点,另外由于我们有Rayleigh和Mie两类散射,还要分别计算不同波长下两种散射的颜色通道,每个顶点共需要2×3×5×(5+5)约300次计算。因此,可以考虑构建查找表T来避免公式(17)中的内积分[O’Neil, 2004],查找表存放在一张纹理上,查找表T的横轴是归一化表示的大气高度h使得x=0代表海平面,x=1代表大气外层天幕半径,纵轴是归一化表示的一束从顶点发出的采样光线的角度θy=0.0代表竖直向上,y=1.0代表竖直向下,查找表T的每一个纹素T (x,y)表示了一个大气高度为x沿着y角度的散射的光线,该纹素的R值存放Rayliegh散射的大气密度ρR,近似取exp(-h/H0),纹素的G值存放该位置Rayliegh散射的Optical DepthtRB值和A值分别存放Mie散射的大气密度ρMOptical DepthtM。查找表T需要预先生产。

在对于一段元路径计算Optical Depth时,会存在不能简单处理的情况。如果摄像机处于宇宙空间中,对于每个采样点,可以简单计算它的高度,从查找表T的G和A通道读出Optical Depth值;如果摄像机处于大气层内,查找表T会出错,由于它存储的Optical Depth值是从一个大气层内的点到大气层顶部,它没有存储一个大气层内的点到另外一个大气层内的点的Optical Depth值。为了解决这个问题,可以建立两个查找表(一个用于采样点,一个用于摄像机点),对查得的两个Optical Depth值作减法运算得到需要的Optical Depth值。另外一个问题是,当一个点在摄像机之上,角度对于摄像机向下,会从查找表T得到错误的值。为了解决这个问题,对于两个查找表均反转光束方向来查找。

查找表T解决了内积分问题,但是仍然需要较长的Vertex Shader来实现,另外在Vertex Shader访问查找表T需要Shader Model 3.0的Vertex Texture Fetch技术支持,因此,希望从数学分析的角度通过观察查找表T,把查找表T转化为一个经验函数来处理。[O’Neil, 2005]介绍了一种近似的多项式逼近方法,根据公式(5),查找表T中的大气密度ρ是高度h的函数,与角度θ无关,那么只需要考察原查找表上x轴高度hy轴角度θ,以及每一项中的Optical Deptht之间的关系假设存在对于Optical Depth的经验函数t(h,θ),首先以高度hx轴,取值范围从0到1,Optical Depth值为y轴,对于从0到1的各种角度θ分别采样,画出单独的曲线在图上。发现当x从0到1变化时,恰好呈现指数递降。为了更好地比较各个曲线的形状,把这些曲线都进行了归一化,发现这些曲线都近似exp(-4h)。参考公式(5)和公式(11),公式(11) Optical Depth值就是对公式(5)的ρ值积分,归一化后当H0=0.25时就是近似e-4h这条曲线。因此,exp(-4h)可以作为一个公因子提出来,Optical Depth值可以近似看作h的函数,定义如下光学深度系数ξ(h):

clip_image054 (18)

接下来分析Optical Depth值与角度θ的关系,以角度θ作为x轴,y轴为角度θ的归一化因子,建立一个新的图像来分析,没有发现合适的曲线可以拟合,再尝试以角度θx归一化θ的自然对数为y轴,也没有发现合适的曲线可以拟合,因此只能采用多项式来拟合,构造一个角度θ的余弦为变量的尺度函数Scale(cosθ)。最终Optical Depth可以表示为:

clip_image056

(19)

其中clip_image058为大气层平均密度值所在的高度。

4.3 Vertex Shader

对于Vertex Shader顶点程序段,首先要计算如图7顶点AB。由于大气天幕网格的输入顶点就是我们需要的B点,那么我们只需要计算散射开始的A点,那么可以先计算从视点E到顶点B的单位化光束向量clip_image060,通过求解光束和大气方程求两者的交点A

clip_image062 (20)

其中O点是地心,也是我们的坐标原点,clip_image064是从视点E到地心O向量,clip_image066是从EB点向量,对clip_image066[1]进行归一单位化,可以得到光束单位向量。RPP点极坐标参数化后的半径,RA是地球大气外圈半径,得到|EA|后我们根据向量运算法则可以得到A点的坐标vA

clip_image068 (21)

那么B点与A点存在下面的关系:

clip_image070 (22)

接着通过点积的形式来计算A点散射角度余弦cosθA

clip_image072 (23)

其中Router是大气层外半径,也就是A点的极坐标半径。A点到O点的向量AO,由于O点是三维空间坐标原点,因此等价于A点坐标vA。接着计算A点的大气高度hA

clip_image074 (24)

其中Rinner是大气层内半径,接着根据公式(5)计算A点的大气密度ρA,接着计算公式(11)中的AOptical DepthtA,为什么这个值不能省略,在大气层外边界上时要计算这个值呢,主要是由于大气密度在大气层外边界不完全为0,密度虽然呈指数递减并且在大气层外边界很接近0,但是如果不计算这个值得话会在摄像机进入大气层时颜色上呈现一刹那跳跃感,不平滑过渡。

得到AB点坐标后我们就可以进行循环采样了,首先通过AB间距除以采样数量计算得到采样间距dsample,接着通过线性插值计算出采样点P1-P5坐标VP。然后,对于每一个采样点P,进行下面具体步骤计算:

1) 根据上面求得的每个P点坐标VP计算极坐标下的半径RP进而计算大气高度hP

2) 根据公式(5)计算P点的大气密度ρP

3) 计算P点对应公式(18)中的ξ(h)

4) 计算太阳光向量与向量clip_image076的点积得到夹角α的余弦cosαP

5) 计算向量clip_image078与向量clip_image076[1]的点积得到夹角β的余弦cosβP

6) 结合4),5)步骤得到P点的Optical Depth

clip_image080

(25)

7) 计算得到公式(16)中每个采样点P散射的光线到视点E点的削弱量

其中公式(25)可以从公式(18)结合图1推导得到。完成上述步骤后,把所有采样点散射到视点E的削弱量叠加,得到公式(17)中的积分部分,再乘上公式(17)中的系数部分中的波长λ等值。因为象素要比顶点多得多,因此希望把散射计算尽可能放在Vertex Shader中完成,因此除了与每个象素相关的相位函数值在Pixel Shader才计算以外,需要把公式(17)中可以在Vertex Shader计算的部分计算好。由于最终的颜色是Mie散射颜色和Rayleigh散射颜色叠加的,对于Mie散射和Rayleigh散射是不同的,分别用两个纹理坐标存放公式(17)中用到公式(12)的G(h, λ, θ)中除FM(θ,g)和FR(θ)外其他部分的值,以输出到Pixel Shader。

4.4 Pixel Shader

在Pixel Shader对大气天幕中的每个象素进行Mie散射颜色和Rayleigh散射颜色叠加。那么剩下来就是要计算相位函数,在公式(3)和公式(8)中,由于反复用到clip_image082,可以先计算clip_image082[1]

clip_image085 (26)

其中,clip_image087为摄像机到顶点的向量,clip_image089为太阳光源到顶点的向量,通过对clip_image087[1]向量和clip_image089[1]向量的点积来计算cosθ。根据公式(3)可以计算得到Rayleigh相位值FR(θ),根据公式(8)计算得到Mie相位值FM(θ,g),Rayleigh相位函数在0度和180度最亮,在90度最暗,对于90度时,整个天幕显得太暗,之所以与实际亮度有点差距,其主要原因是没有进行多重散射计算。得到相位函数后再乘以Vertex Shader传来的其他部分即得到了Mie散射和Rayleigh散射的颜色,把两种颜色叠加起来即得到最终颜色。

5 结果与讨论

我们在NV 8400M GPU上进行了实现,图8给出了试验的效果图。实验机器GPU NV8400m,内存 2G,实验中开启大气绘制效果时渲染帧数为91关闭大气绘制效果时渲染帧数为132,即每帧消耗约3.413毫秒时间绘制大气散射效果。如图8,图8(a)为从宇宙观察地球时大气散射的效果,图8(b)为北京市上空傍晚的散射景象。图8(c)和(d)为极点地区大气散射效果。另外,我们已经应用把大气散射效果应用在为苏州互动创意有限公司(ICT)所开发的数字地球漫游平台客户端上。

clip_image091

(a)

clip_image093

(b)

clip_image095

(c)

clip_image097

(d)

图8:大气散射效果

参考文献

1. O’Neil S. Real-time Atmospheric Scattering, GammeDev.net, 2004

2. O’Neil S. Accurate Atmospheric Scattering, GPU GEMS2. 2005. 253~267

3. Nishita T, Sirai T, Tadamura K, Nakamae E. Display of The Earth Taking into Account Atmospheric Scattering. Proceedings of ACM SIGGRAPH 1993. 27(4): 175~182

4. Nishita T, Dobashi Y, Kaneda K, Yamashita.H. Display method of the sky color taking into account multiple scattering. Proceedings of Pacific Graphics 1996, 117~132

5. Nishita T, Dobashi Y, Nakamae E. Display of clouds taking into account multiple anisotropic scattering and sky. Proceedings of ACM SIGGRAPH 1996, 30(4):379~386.

posted @ 2011-09-08 17:58 ~哇哇~ 阅读(429) 评论(0) 编辑

GPU三维图元拾取

张嘉华 梁成 李桂清

(华南理工大学计算机科学与工程学院 广州 510640)

(newzjh@126.com)

摘要:本文探讨了两种新颖的在GPU上实现的三维图元拾取方法,第一种方法是场景几何无关的,通过将坐标信息和对象面片指针绘制到一张Render Target型浮点纹理实现三维对象拾取。第二种方法是场景几何依赖的,对逆变换到世界空间的拾取射线与各个几何图元在Geometry Shader下逐一求交.上述方法经过实验能够在约半帧时间内拾取几何图元的指针信息和坐标信息,达到与屏幕象素大小同等的精确度。

关键词:图元拾取、GPU、几何Shader、渲染到纹理、浮点纹理、三维交互

3D Primitive Picking using GPU

Zhang Jiahua Liang Cheng Li Guiqing

(College of Computer Science and Engineering, South China University Of Technology , Guangzhou , 510640)

Abstract This paper presents two novel 3D picking approaches implemented based on GPU.The first approach is scene geometry independent. It picks a 3D Primitive by rendering the position information and point information of geometry primitives to a Render Target floating Texture.The second approach is scene geometry dependent. It calculates the picking ray by inverting the transforms from the world space to the projection space,and then Intersects each primitive with the ray using a Geometry Shader.The approaches can pick the position and point of primitive within half frame time on GPU. Especially the first one can achieve pixel precision.

Key words Primitive Picking,GPU,Geometry Shader,RTT,Floating Texture,3D Interaction

clip_image002

图1:拾取RRT后的浮点纹理,图中RGB颜色值显示出来的是对象的世界坐标值,如左面的对象1顶部比较绿,反映图元的y值坐标比较大,对象的rgb值分别对应xyz坐标,alpha值对应图元指针

三维图元拾取技术,在计算机动画编辑,三维游戏交互过程中经常用到,如鼠标拾取地形或网格上某个图元(点,线,面片)等操作。随着现代GPU的迅速发展,GPU可编程能力带来了很高的灵活性。Shader Model 4.0的GPU中已经可以进行Vertex Shader,Geometry Shader,Pixel Shader三个阶段的GPU编程.Shader是我们自己定义的程序,替代固定渲染管线中的部分流程,实现图形特效和通用计算目的(GPGPU)。因此,过往无法利用GPU加速计算的拾取过程现在可以成为现实.

本文探讨了两种基于GPU的图形拾取方法。第一种方法与场景几何无关,在shader中进行坐标和指针到颜色的转换编码,把世界坐标和对象指针绘制到一张128位R32G32B32A32浮点RenderTarget(Opengl中的PBuffer)型纹理,然后把浮点纹理中的指定象素值拷贝回CPU,判断该象素的值实现几何图元的拾取,第二种方法是场景几何相关的,依赖于场景n元树分割,通过迭代在Geometry Shader中找到相交图元再把结果利用Stream Out特性送回IA缓冲.GPU拾取方法实质上是把GPU用于通用计算目的而非渲染目的.

1. 相关方法

拾取一般都被看作渲染的一个逆运算。主要分为场景几何依赖和场景几何无关两类方法.

场景几何依赖方法是将鼠标选中的点从屏幕坐标系逆变换到世界坐标系,然后把拾取的点看作射线在世界坐标系与各个几何图元求交,找出被选中的物体。其中较有代表性的是姚继权等[1]和王剑等[2]的工作.一般的顶点渲染管线将一个物体从本身的局部空间,转换到统一的世界空间,然后根据视锥转换到观察空间,经过背面剔除、光照、裁剪等处理后,投影(Projection)到二维的平面上,最后根据显示的环境进行视口变换和光栅化,最后在Render Target显示出来.而拾取则是在Render Target上鼠标点击了某点或者圈定了某个范围判断选取了世界空间下那些图元对象,姚继权等[1]和王剑等[2]的工作将拾取的二维坐标加上第三维深度从投影空间逆变换回世界空间下.将逆变换回世界空间的点设为一条射线,跟世界空间内的物体按深度顺序逐个求交.场景几何依赖方法需要考察场景几何的复杂度,如果场景对象网格有较多几何图元,逐一求交的运算量会非常大。姚继权等[1]和王剑等[2]的做法是计算物体的包围球,然后将射线与包围球求交。一般地,场景几何依赖拾取可以分解为下面四个步骤:

(1) 获得屏幕上的点s,找到他对应的投影窗口上的点p;

(2) 计算拾取射线,它是一条从原点出发穿过点p的射线;

(3) 将射线乘以观察矩阵和投影矩阵连乘后的联合矩阵的转置逆矩阵,变换到和模型相同的坐标系中;

(4) 判定物体和射线求交,被穿过的物体就是屏幕上拾取的物体

场景几何无关方法是正向将场景图元的几何信息直接渲染到Render Target,当用户交互点击或圈定拾取图元时直接判断Render Target对应象素位置的图元信息.但是这种方法在浮点纹理和Shader Model 2.0以前由于GPU硬件能力限制一直没法实现.

2. 渲染到浮点纹理的场景几何无关拾取

场景几何无关方法的应用一直受到限制:它需要GPU的可编程能力,需要Shader Model 2.0和浮点型纹理支持;其次是因为每次出发拾取程序的时候都要在后台绘制一张表面,等同于重复绘制了两次场景,不适合频繁连续的拾取动作。

我们方法的核心是把图元的几何信息、指针等相关信息作为它的颜色渲染到后台的一张Render Target型表面上对应帧缓冲做一次特殊的“渲染”。在渲染后我们只要读取表面上相应象素坐标的颜色值就可以得知它相对应图元的信息。整个算法分为在CPU上和在GPU上的两部分。在CPU上C++程序的任务是建立一个后台的表面,然后调用GPU上的程序对物体进行特殊的RTT(Rander to Texture,渲染到纹理),再根据渲染结果读取表面某坐标的颜色并还原信息。具体实现时考虑到纹理有pow of two 和non-pow of two之别,我们的屏幕分辨率一般不为pow of tow,因此可以考虑采用渲染到表面(RRS),用Direct3D9中的CreateRenderTarget()创建一个Render Target型表面,CPU上的主要流程如下:

鼠标点击拾取事件触发下面流程:

(1) 建立一张新的Render Target型临时纹理。

(2) 将当前设备屏幕的内容存入缓冲中,将设备的渲染对象设为该临时纹理。

(3) 做好渲染前的准备,包括从fx文件中读入effect。

(4) 对于每个几何对象在GPU通过Shader进行特殊渲染。

(5) 还原设备信息,设备的渲染对象指向帧缓冲。

(6) 获取临时纹理上拾取区域的象素。

(7) 将获得的象素按定义的格式解码。

我们首先如代码1,定义Vertex Shader的输出内容,定义了一个包括坐标和颜色的结构(struct)。其中pos用于顶点转换,color用于顶点信息存储.

在Vertex Shader中,输入某点的局部坐标,把坐标的值编码成一个颜色的RGB值后作为输出颜色的RGB值,同时将输入坐标向量左乘以世界矩阵、观察矩阵、投影矩阵后得到最终输出的坐标值。在Pixel Shader中,我们将物体指针信息作为颜色的Alpha值,加上Vertex Shader输出的颜色,输出为最终的渲染颜色。

在单个几何图元中,不同顶点有不同的坐标,在Vertex Shader中,我们根据坐标信息进行顶点转换,同时将坐标信息作为颜色值存入VSResult中。主要计算参阅代码1:

struct VSResult

{

float4 pos:POSITION;

float4 color:TEXCOORD0;

};

VSResult VS_main (float3 pos:POSITION)

{

VSResult ret;

ret.color=float4 (pos,1.0f);

float4 worldpos=mul(float4(pos,1),worldmatrix);

ret.pos=mul(worldpos,ViewProjection);

return ret;

}

代码1:场景几何无关拾取方法的Vertex Shader

对于ret.color,前三个RGB值表示输入的位置坐标;而第4个Alpha值用作存储其他信息,暂时填入1.0f,后文将填充图元的指针。利用以上方法,我们可以得到如本文开头的图1的一张标有坐标信息的彩图。

除了坐标外,还可以存储其他信息. 如物体对应的实体或者网格之类的指针。其特点是同一物体的信息是一致的,也就是说相对于Shader中同一次DP(IDirect3DDevice9::DrawPrimitive)或者DIP(IDirect3DDevice9::DrawIndexedPrimitive)调用是一个常量,在Shader中标识uniform,作为一个全局变量,在CPU中通过ID3DXEFFECT::SetFloat()进行设置,再在Pixel Shader中加入到顶点的颜色信息中。Shader中的主要代码如下:

uniform float pobj;

float4 PS_main (float4 color:COLOR):COLOR0

{

return float4(color.rgb,pobj);

}

代码2:场景几何无关拾取方法的Pixel Shader

在C++的程序中,可以把每个网格的指针转换为long值,再转换为float值,每个网格绘制时通过ID3DXEffect::SetFloat()传递到GPU中,如果拾取需要判断到图元,则需要对每个顶点将图元的指针放到顶点的纹理坐标上,作为输入顶点的纹理坐标传输到GPU.

信息解码主要在CPU上完成,因为只需要解析选中的点。基本流程是:用GetRenderTargetData()拷回纹理的一个系统内存副本;然后调用LockRect()锁定该副本纹理;将拾取的点的各个通道值转换成float*读出,其中数组的0123位依次是颜色的RGBA值;最后将象素的Alpha转换为Long值再转换为对象或图元的指针。

在RTT产生的纹理中,我们不仅加入了坐标信息,而且加入了指针信息。通过指针传递我们可以记录关于图元几何的各种信息。但是指针对纹理存贮的精度要求很高,不能存在丝毫误差。在32位的系统中,一个指针占4字节,在C++程序中,我们采用long类型进行传递,而传入Shader时我们采用了float类型。而对于绘制时所用的纹理,我们必须保证A通道有8×4,也就是32位,所以我们采用了D3DFMT_A32B32G32R32F格式,即32Bit IEEE Float格式的纹理。

对于拾取所得的纹理,可以简单地直接调用LockRect()锁定纹理最高精度表面检索纹素(texel)值。然而,直接调用Lock()函数锁定可能会让CPU等待GPU完成当前的绘制操作再进行Lock()操作,为了提高并发性,可以采取readonly标识和GetRenderTargetData()函数把Render Target纹理拷回到系统内存的一个纹理上再检索。GetRenderTargetData()能够把GPU上Render Target的一个纹理直接完整拷贝到内存,再用带D3DLOCK_READONLY和D3DLOCK_DONOTWAIT等标识锁定要拾取的象素。

3. Geometry Shader下场景几何依赖拾取

场景几何依赖的图元拾取方法将屏幕投影坐标下的拾取象素坐标逆投影回世界坐标,与视点一起构成三维射线向量。关键的一步是将该向量与场景几何里面的图元逐一求交判断是否相交,对相交结果根据投影深度取最前的图元作为结果.如果场景对象网格有较多几何图元,逐一求交的运算量会非常大,如果对场景世界空间进行了n元分割,如八叉树分割,那么可以通过迭代逐步缩小与向量相交的空间,达到一定阈值后再对该子空间内的图元逐一求交.

图2描述了本文探讨的GPU场景几何依赖拾取的流程,用户点击鼠标或圈取屏幕一个区域触发拾取流程,首先把拾取的屏幕坐标加上深度转换为投影空间坐标,左乘以投影矩阵的逆矩阵得到观察空间的坐标:

clip_image004 (1)

再左乘观察矩阵的逆矩阵得到世界空间的坐标:

clip_image006 (2)

通过(2)我们得到了拾取点在世界空间的坐标,接着根据观察矩阵的逆矩阵计算拾取射线方向:

clip_image008 (3)

clip_image010

由此,我们得到了世界空间下的拾取射线,需要注意的是Direct3D9用的是左手坐标系,而OpenGL和Direct3D10默认右手坐标系.得到拾取射线后判断场景的图元几何是否复杂是否可以进行空间八叉树分割,如果可以则在Geometry Shader中找出一个合适的八叉树子空间,接着对子空间内所有图元在Geometry Shader根据深度逐一与拾取射线求交,如果不可以八叉树分割则直接把整个场景看作八叉树的一个节点,对节点内所有图元在Geometry Shader根据深度逐一与拾取射线求交.

clip_image011

图2:GPU场景几何依赖拾取流程

Shader Model 4.0以及Geometry Shader的出现使得我们能够在GPU实现图元的几何拾取,Geometry Shader添加了对各种几何图元输入的支持,输入的图元可以是点,线,三角形以及带有邻接图元的这三种图元对象流,我们可以把这个迭代过程和图元求交过程在Geometry Shader实现,然后把结果直接利用Stream Out输出回IA或通过Pixel Shader绘制到某个一个象素的Render Target,再检索该象素的值.

在Geometry Shader中,输入是整个世界空间的两个极限位置坐标vMin,vMax,迭代调用IntersecAABB()判断AABB的两个对角三角形是否与拾取射线相交,直到求得一个满意大小的八叉子空间,IntersecAABB()首先调用IntersectTriangle()判断该空间的两个对角三角形是否与拾取射线在世界空间相交,如果相交则迭代调用八个子空间的IntersecAABB(),当达到指定大小后输出该子空间的vMin和vMax回IA .在Shader Model 4.0中,Shader的长度不再受到限制,因此可以满足在函数内迭代调用八个子空间IntersecAABB()函数.得到某个子空间后,把该子空间内所有图元根据深度与拾取射线在世界空间内进行比较.代码3描述了这个过程.现代GPU具有很高的计算性能,对于场景几何复杂或不容易进行八叉树空间分割的场景可以直接对拾取射线和各个图元进行求交.整个过程通过ID3D10Device::DrawAuto()实现.

[maxvertexcount(20)]

PS_INPUT GS(triangle GSIn input[2], inout TriangleStream<GSOut> TriangleOutputStream)

{

IntersectAABB(input[0].pos, input[1].pos);

}

bool IntersectAABB(float3 vmin,float3 vmax)

{

if (tilesize<1)

{

TriangleOutputStream .Append(vmin);

TriangleOutputStream .Append(vmax);

TriangleOutputStream.RestartStrip();

}

If (调用IntersectTriangle()判断两个对角三角形是否与拾取射线相交)

{

对8个子空间调用IntersectAABB函数

};

}

bool IntersectTriangle(float3 v0, float3 v1, float3 v2)

{

float3 edge1 = v1 - v0;

float3 edge2 = v2 - v0;

float3 pvec=cross(dir,edge2);

float det=dot(edge1,pvec);

float3 tvec;

if( det > 0 )

{

tvec = orig - v0;

}

else

{

tvec = v0 - orig;

det = -det;

}

return det<0.0001f?false:true;

}

代码3:Geometry Shader中迭代求子空间与判断拾取射线与三角面片是否相交的代码

clip_image014

图2 :DrawAuto()函数描述的概念图[5]。

如图2,当数据利用Stream Out输出到显存SO段缓冲后,能够改变视图把这些数据再次作为Input Assembler输入,DrawAuto()函数会绘制它们,并且无需应用程序知道数据的数量和缓冲的大小.CPU不需要获得数据数量以重新绑定该缓冲从SO段到IA段.尽管无需知道数据数量,但是应用程序需要负责把这些数据从SO段再绑定到IA段的格式输出描述.

4、结果与结论

我们在VC++8.0实现了上述两种方法。在程序中我们添加1个网格,取名为obj1,程序运行后实时渲染该网格,并且在鼠标单击进行拾取操作后弹出对话框说明选中物体名称以及所在坐标。对于图3的场景,可以准确拾取到图元对象的的指针以及图元的世界空间坐标(方法一可以得到对象空间坐标).对于本文方法一,由于等同于再渲染了一次场景几何图元,因此其精度等同于帧缓冲的象素精度,鼠标点击屏幕那个象素就是那个象素对应的图元世界空间坐标,而且不需考虑遮挡投影等几何关系,这些在顺向过程中由渲染流程处理了.而对于本文方法二,其精度为浮点精度.对于同一深度相互部分遮挡的图元如果不求出交点,为了快速计算根据本文代码只判断图元与射线是否相交可能得到多个拾取结果.

clip_image016

图3 三维拾取示例,鼠标拾取了obj1上某个三角形图元,应用程序报告拾取的指针和坐标

我们还和姚继权[1]等工作中描述的射线求交方法进行对比. 它们的方法能够让重叠的物体与射线相交得到交点,根据交点与视点的远近确定拾取的图元,在CPU上实现速度较快.我们在NV FX8800 GPU环境下实现了姚继权[1]的方法,并且在没有构建八叉树的场景中进行了对比,表1是本文描述方法与之的对比.

表1:各种图元拾取方法的对比

时间

场景1:如图2两个模型共含2223个图元

场景2:含120484个图元

每帧时间

3.20毫秒

12.19毫秒

姚继权[1]等的CPU射线求交

0.99毫秒

50.74毫秒

本文方法一:渲染位置和坐标到浮点纹理实现拾取

1.43毫秒

5.14毫秒

本文方法二:在Geometry Shader下实现迭代和求交

0.87毫秒

3.74毫秒

我们通过Win32下的微秒级精度时间检索函数QueryPerformanceFrequency()和QueryPerformanceCounter()检索时间,将每个操作前后两个时间相减得到时间差.对于GPU操作时间目前我们也只是在CPU上通过上述函数统计.在NV FX8800 GPU下对于普通复杂程度场景,如图2每秒绘制312帧场景左右,本文描述的方法一需要再绘制一次场景中的需要拾取的大部分几何图元,但其Vertex Shader与Pixel Shader复杂度略低于渲染时所需要进行光照等计算的复杂度,在GPU进行特殊渲染时间约为0.2845帧(拾取时间/一帧时间,这里用帧比例计算主要为了在不同GPU下能有接近的实验结果),接着通过AGP/PCIE带宽返回结果到CPU,时间约为0.3748帧,由于CPU可能存在停滞和同步等待不同[4],总的拾取时间不能简单把特殊渲染时间加上CPU等待结果时间,需要实际测量,这里共需约0.4452帧=1.43毫秒时间,尽管GPU计算很快.但是存在接近固定的CPU等待GPU传回结果的时间,对于图元较少场景要比姚继权[1]等的CPU方法慢.本文描述的方法二在GPU上利用Geometry Shader加速了迭代和求交,把CPU上图元求交计算移到GPU上进行,其计算时间主要依赖于GPU对不同场景几何复杂度的计算能力,速度要比CPU方法略快.对于较为复杂的场景2,每秒绘制82帧场景,大量图元的绘制对于GPU来说是一件能够快速批量处理(Large Batch and Few DIP Call)的任务,GPU绘制每个图元的速度远快于CPU上对每个图元进行求交的几何计算,因此文本描述的方法一主要时间消耗在从GPU传递结果到CPU,总体拾取时间要比CPU方法短,对于支持Shader Model 4.0的GPU,三角形图元能够利用Geometry Shader大大加快了求交速度,本文描述的方法二在速度上优势更加明显.由此可见,对于现代高性能GPU来说,大量通用目的的计算不会是问题,利用GPU进行拾取对于大量图元场景具有较大速度优势.

参考文献(References)

1. 姚继权,李晓豁. 计算机图形学人机交互中三维拾取方法的研究. 工程设计学报,2006, 13(2): 116-120.

2. 王剑,陆国栋,谭建荣. 三维场景中图形对象的拾取方法. 机械,2004, 31(7): 29-32.

3. 何健鹰,徐强华,游佳. 基于OpenGL的一种三维拾取方法. 计算机工程与科学, 2006, 28(1): 45-46.

4. Michael Wimmer, Jiri Bittner. Hardware occlusion queries made useful. Vienna University of Technology,GPU Gems 2 Chapter 6.

5. Microsoft DirectX SDK帮助文档,June 2007.

posted @ 2011-09-08 17:53 ~哇哇~ 阅读(235) 评论(0) 编辑
摘要: Geometry Clipmaps: Terrain Rendering Using Nested Regular Grids 几何裁剪图下的使用嵌套的规则格网渲染地形 使用粗糙的geometry clipmap插图 (size n=31) 216,000×93,600美...阅读全文
posted @ 2011-09-08 17:52 ~哇哇~ 阅读(155) 评论(0) 编辑