哥们网

边做游戏边划水: 基于浅水方程的水面交互、河道交互模拟方法|算法|散度|尺度|双线性

时间:2023-09-28 17:27:23   阅读:154

以下文章来源于腾讯游戏学堂,作者Byreave

篇一:基于浅水方程的水面交互

本文主要介绍一种基于浅水方程的水体交互算法,在基本保持水体交互效果的前提下,实现了一种极简的水面模拟和物体交互方法。

真实感的水体渲染在现今的游戏中越来越被需要,除了光照和波形渲染之外,水体交互也是描述水体功能的重要组成部分。作为一个游戏玩家,同时也作为一个游戏开发者,每到一款游戏中探索时,如果走到水中有较真实的交互效果,总会有种惊喜的感觉,并且也能提高游戏乐趣。玩水谁不爱呢。

水体交互其实也可以分成很多个部分,本文主要讨论的是水面交互,这种交互在游戏中比较常见,也可以说是整个水体渲染中比较低垂的果实。

一些水体交互的例子

下面列举了一些近年比较成功的真实感渲染游戏的水体交互例子。其中大部分都是粒子、纹理和物理结合的方式。

• 老头环 Elden Ring



•刺客信条 英灵殿



• The Last of us Part I Remake



• The Last of us Part II



•荒野大镖客2



•Hogwarts: Legacy



也有一些看起来是没有物理,仅凭借纹理+粒子实现的交互效果,多用于弹道射击水面等场景。在更久远年代的游戏中使用广泛。列举了一些近期的例子:

•Far Cry 6



•荒野大镖客2



•外卖模拟器Death Stranding



可以发现,大部分水面交互都是大同小异,因为游戏对渲染实时性的要求,很多物理模拟步骤会被简化很多,并且会有粒子效果等的帮助,来实现水面交互的真实性。这些交互场景有一些共同的特点,

•只和水体表面的交互,深度不重要

•可交互区域没有明显高度差(水平)

•交互范围有限,大部分围绕角色周围

这些特点基本忽略了三维的模拟,非常适合在游戏中出现较多的,把水体作为单层网格渲染的情况。

那么,有没有一种能够以很小渲染消耗来实现的物理模拟方法呢?本文接下来就会介绍一种基于浅水方程(Shallow Water/Wave Equation)的极简模拟方法,并且集成在了最新的UE5.1版本中。

基于浅水方程的水面模拟方法

话不多说,先看在UE5.1中,使用基本素材做出的交互效果:



水体的表达

对于被模拟的水面来说,游戏中一般可交互的水面都是由一层单面mesh渲染,平面比较多,有坡度也不会太大(比如瀑布就不行)。这种情况我们可以用一张高度图来表现模拟区域,其中水面高度细节可以通过高度图的像素值表达。这给我们在GPU上模拟水面物理带来了便利,可以用贴图(Texture)轻松的求解水面高度。

浅水方程

Shallow Water Equation,也可以叫Shallow Wave Equation,中文浅水方程,是一种流体力学模型,常用于模拟海洋与大气层的流动,适用于分析水平方向的尺度远大于垂直方向的尺度的流体自由液面的流动。从之前的使用场景来看,大部分几乎忽略了“垂直方向的尺度”,经过简化后,非常适合在游戏中实时使用。数学复杂的推导可以参见Wikipedia页面(https://en.wikipedia.org/wiki/Shallow_water_equations),简化过程可以在Games103课程(https://www.bilibili.com/video/BV12Q4y1S73g/?p=10)中学到。我们先忽略数学上的复杂性,直接摆出写方程迭代代码需要的简化结果:



β:ViscosityConstant, α: SWE constant, hN(t-1): Height sum of the nearest four points

可以看到,在简化过的模拟方程中,当前帧水面高度h(t)由前两帧的高度h(t-1)和h(t-2)得到。总体代码实现并不复杂,由于我们的简化加入了额外的Damping参数加快迭代,其它参数设置推荐β设置为1,α设置为0-0.5的值,Damping设置为0-1的值,以免出现模拟爆炸。

边界条件

水体模拟的区域往往是有边界的,无论是在小水洼的边界,还是无边界海洋中的石头,都可以作为模拟的边界信息。对于边界的处理往往有两种方式,一种是不处理,水波会根据阻力慢慢消失,这样以真实性为代价换来性能的提升,在开放的水域比较适用。第二种是把模拟的水波反弹回来,在游泳池之类的明显边界的水域比较适用。比如:



模拟方程中对于第二种边界处理方法的描述也相对简单,叫Neumann boundary condition(https://en.wikipedia.org/wiki/Neumann_boundary_condition),简单来说就是描述边界上的导数,导数为0则在边界上不会有变化。我们可以看到上述方程中有hN(t-1)项,既当前模拟点,假设为X,周围4点的水面高度和。想要做到反弹水面,我们在查询周围4点水面高度时,如果4点中某一点Y在边界上,把Y的高度设置为和X点高度相同即可。物理意义上,就是指阻止边界点的水面高度交换,这样在模拟过程中,水面波形就可以产生反弹的效果。可以看看下面的伪代码。

// Suppose Y is the point up to X. YIndex = XIndex + (0, 1);

float XHeight=WaterPreHeight.Load(XIndex).x;

float YHeight=WaterPreHeight.Load(YIndex).x;

if(IsBoundary(YHeight))

YHeight=XHeight;

模拟步骤

本文介绍的模拟方法大致分为如下几步:

1. 收集和水面模拟区域产生交互的物体,并且渲染物体深度信息到贴图。

2. 叠加物体信息到水面高度图。

3. 模拟方程迭代。

4. 应用模拟结果的高度图来渲染水面。



收集物体信息

想要知道有哪些物体和水面交互有很多种办法。我们的方法可以只作为参考。从效率上考虑,可以为模拟区域设置一个碰撞体,通过碰撞系统从GameThread得到所有的相交物体,然后提交到渲染线程,获得物体的信息,交给模拟迭代。上面提到,我们可以通过贴图表达水面高度,我们也可以通过顶视图的深度信息来表达物体和水面交互的信息。现在比较棘手的问题是,如何高效的渲染物体的深度。

在UE引擎中,我们可以通过SceneCaptureComponent来获得水面物体深度,它支持只渲染场景中部分物体。但是在看过代码之后发现,SceneCapture需要走完整个渲染流程,哪怕我们只需要一个深度信息,也需要付出渲染整个场景的代价。SceneCapture也无法定制渲染所用的Shader,当然可以用自定义后处理材质,但是这样又会带来新的性能消耗,所以可以想象我们需要一个更加简单、高效的流程来完成物体信息的收集。

在我们的实现中,我们实现了一个自定义的深度渲染pass,用来渲染指定物体的深度信息。其原理和阴影贴图渲染(ShadowDepth)类似,根据UE官方文档(https://docs.unrealengine.com/5.1/en-US/mesh-drawing-pipeline-in-unreal-engine/),



通过添加自定义的FParallelMeshDrawCommandPass来把收集到的物体渲染到自定义的DepthBuffer中。这种办法的好处是可以自定义MeshProcessor,支持修改CullingMode和使用自定义的PS,这在后续获得精确深度的步骤中有很重要的作用。

在切换到自定义的Pass实现后,获取物体信息这一步的耗时也明显降低。从之前SceneRenderer使用的一整套渲染流程,变成了一些简单的深度DrawCall的代价。

在获取物体信息上,除了运动的物体信息,比如玩家角色、移动物体、子弹等,还需要绘制作为边界物体的信息。边界物体信息我们可以做进一步优化,比如每帧只渲染正在运动的物体,静止的物体根据设置,判断是否要渲染成边界,而边界信息不需要每一帧更新,只有在边界变化的时候按需求更新。边界的信息可以保存成一个顶视图深度图,在后续步骤中重复使用。

对于深度图,因为根据算法原因,我们对于物体浸入了水面多少其实不敏感(垂直方向尺度非常小,所以叫做“浅”水方程),这样就还可以进一步优化。添加一个ResolvePass,只使用R8格式存储深度贴图。这样有利于后续步骤中贴图读取的开销。



在UE Insight中可以看到,同时渲染9个移动的物体和角色,加上ResolvePass的开销只需要0.08ms(虽然是在2080S上)。

叠加物体信息到水面高度图

为了使耗时较高的迭代Pass读取贴图次数减少,我们需要预处理我们的物体信息。其中,我们在获取物体信息阶段获得了2个R8的贴图(运动物体和边界物体信息)。我们需要将运动物体信息+边界信息整合进水面高度图。

对于运动物体信息,我们需要将有物体浸入的水面排出一些水,模拟水面交互的过程。对于水面高度减少的多少,我们做了进一步简化,只需要根据深度的减少就好(物理正确的结果需要迭代求解减少的高度,原理解释参见Games103课程(https://www.bilibili.com/video/BV12Q4y1S73g/?p=10))。对于水面高度已经在物体深度之下的水面,我们不做处理。

对于边界物体信息,我们把水面高度设置成一个特殊值,比如float_max即可。伪代码如下:

if(bIsBoundary)

WaterHeight[SimulationIndex]=BOUNDARY_HEIGHT;

else

CapturedDepthVal=saturate(CapturedDepthVal);

float CurrentWaterHeight=WaterHeight[SimulationIndex];

// Object depth above water has no effect

WaterHeight[SimulationIndex]=CapturedDepthVal==0.0f?CurrentWaterHeight:min(CurrentWaterHeight,-CapturedDepthVal);

模拟方程迭代

有了上述物体信息,我们需要完成方程的迭代。在传统方法中,模拟方程并没有最外层的Damping参数,需要在同一帧多次迭代,求解收敛值。这里的多次迭代,包括了方程本身的迭代,和上文提到的对交互物体陷入水面后,水面高度应该变化多少的求解。方程本身的迭代我们可以通过多次运行CS求解,而水面高度变化因为不仅和物体陷入的深度相关,也和周围水面的高度相关,所以需要用到共轭梯度法(PCG)求解。具体算法可以参考Games103视频最后有关VirtualHeight的求解(https://www.bilibili.com/video/BV12Q4y1S73g?t=4855.3&p=10),和文末参考信息中的原始论文。

在我们的实现中,简化了多次迭代的过程(参考WaterLinePro插件中的实现),每一帧只运行一次迭代,这样在保证结果真实性的情况下有最好的性能。一次迭代的缺点就是波纹传播的速度和游戏的帧率相关,可以想象目前每帧一次迭代,根据方程波纹每帧只能影响周围一个像素,水面波纹信息每帧只能传递一个像素。帧率较高的应用或者对于传播速度有调整,可以调整上述模拟方程中的α参数或者模拟范围和模拟分辨率的比例,来保证传播速度符合理想。

对于Shader中的实现,我们有了2张前一帧和前两帧的水面高度图作为输入,1张当前帧的水面高度图作为输出,就可以通过CS/全屏PS完成迭代。在实现过程中,可以通过ping-pong三张贴图的方式,这样不需要额外的复制操作就可以记录水面高度历史信息。

模拟迭代比较直接,下面是伪代码:

// Boundary pixels.

if(IsBoundary(PrevHeight))

// Skip iteration.

return;

// SWE

float NearHeight=UpPrevHeight+DownPrevHeight+LeftPrevHeight+RightPrevHeight;

float OutHeight=Damping*(PrevHeight+(PrevHeight-PrevPrevHeight)+TravelSpeed*0.5f*(NearHeight-PrevHeight*4));

// Store to output texture

OutWaterHeight[CurrentIndex]=OutHeight;

应用模拟结果

完成每一帧的迭代后,我们就获得了可以使用的水面高度贴图。贴图预览结果如下:

左边为移动物体信息,右边为迭代的水面高度



在我们的实现中,使用ENQUEUE_RENDER_COMMAND来完成对模拟步骤的提交,会在主渲染帧之前完成,这样既不会破坏现有的渲染管线,也可以把当前帧的模拟结果直接使用到水体材质中,完成水体的Normal/Displacement计算等操作。

有了高度图,我们可以在水体系统中得到Normal,并且应用到水体材质中。我们目前使用的自研PhotonWater系统通过提前的Normal/Displacement Pass来完成计算,结果很方便的就可以应用于水体。

值得注意的是,虽然在上述的游戏示例中,大部分水面都有位移计算,但是部分情况我们只需要计算Norma即可(尤其是手机端)。不过有了水面Mesh的位移的确可以提高真实性。PhotonWater实现的基于CDLOD的水体Mesh Tessellation和Screen-Space Displacement Mapping可以比较高效地应用各种位移贴图和波纹粒子效果。

获取准确的物体交互信息

至此,大部分水体模拟的实现细节就完成了。虽然还有很多可以改进的地方,我们还是可以先关注比较容易且效果好的改进。在上述示例的荒野大镖客2的例子中,我们可以看到水面的交互只和马的腿部产生。这种细节如果我们只用了正常渲染的顶视图深度是不够的,因为顶视图会认为马的身体遮挡了水面,从而把整个马作为深度信息传入,就无法做到上述细节。

比较直观的例子是一个倒立的圆锥体,如果圆锥体落入水面,我们只希望尖的地方和水体产生交互,而不做处理的方法会让交互范围变成圆锥的帽子,比如下图:



那么怎么做,才能让圆锥的尖角和水面“碰撞”呢?



解决方法其实有很多,其中比较精确的办法是通过自定义顶视图的深度渲染,通过渲染交互物体Mesh的反面,然后对水面深度进行Clip,就可以获得精确的反面深度信息。实现层面就是在BuildMeshDrawCommand之前手动反转CullMode。

ERasterizerCullMode MeshCullMode=ComputeMeshCullMode(MaterialResource,OverrideSettings);

// Always render back faces

MeshCullMode=MeshCullMode==CM_CCW?CM_CW:CM_CCW;

BuildMeshDrawCommmands(

...,

MeshCullMode,

在深度渲染的PS中(注意一般的深度渲染Pass不需要PS,这也是我们上文提到的自定义深度渲染的便捷点之一),我们传入模拟位置的水面高度,不需要太精确,可以暂时假定模拟区域是一个平面即可。内容也可以非常简单。

// CustomCapturePS.usf

// Clip using water level, water level is already converted to device Z

clip(WaterLevelVal-SvPosition.z);

另外一种解决办法是使用一个替代(Proxy)Mesh渲染高度,而不是用真实物体渲染。部分游戏,比如Hogwarts: Legacy游泳的场景,细节要求不需要太高,可以不渲染整个角色Mesh,而是渲染一个替代品。比如一个简单的Sphere/Capsule代替人物,好处是显著减少三角面,缺点是需要Artists手动放置到角色上,以防止穿帮。在马匹的例子中,也可以用4个小球,Attach到马腿上,并且忽略马匹本身的Mesh,这样也可以完成交互细节的渲染。

显存和Shader消耗

本文介绍的方法使用了多个Buffer用于存储模拟数据。其中Buffer的大小可以由用户自定义,示例中使用的是1024x1024贴图,用来渲染4096x4096世界大小范围的水域。贴图分辨率可以根据性能需求调整,水域范围也可以由实际应用决定。

•3个水面高度图 R16F * 3

•深度信息和边界信息 R8 * 2

•渲染深度所用的DS * 1



模拟流程上用了这些Pass:

•渲染运动物体(如果有)-CustomCapturePass

•渲染边界物体(如果有更新)

•Resolve深度信息-PhotonWaterCopyCaptureDepth

•叠加深度信息到水体高度-ComputeWaves

•迭代模拟方程-IterateSWE

在模拟1024X1024分辨率的贴图情况下,UnrealInsight显示的消耗如下(2080S):



还有一些可以减少消耗的优化技巧,比如模拟区域没有物体更新,可以过一段时间暂停更新,等有交互出现的时候再启动,以免出现空转的情况。

移动端也能用

在移动端,模拟过程类似,模拟效果来看可以达到和PC/Console差不多的效果。



关于移动端的耗时,笔者没有统计各个Pass分开耗时,只能从开关模拟来看总体耗时,在三星S20手机上得到以下结果:

• 1024x1024的模拟分辨率,模拟开销大概3ms。

• 512x512的模拟分辨率,模拟开销大概在1ms以下。

在分辨率降低的情况下,只要模拟区域的世界大小也等比例降低,得到的波纹细节是一致的,所以可以根据使用场景合理调节。在不需要准确波纹细节的情况下,256x256模拟分辨率也是可以选择的。

篇二:游戏中的河道交互模拟方法

以上简单介绍了浅水方程的简化模型,以及应用它来进行水面交互。在湖泊、池塘、小水洼等水面区域,交互效果表现还算不错。但是在游戏中,河道的应用也非常多。那么,在一个流动的河道中,我们怎么模拟出水面交互效果呢?

下面主要介绍一种在河道模拟水面交互的方法,让交互感更加真实。同样是应用前文提到的浅水方程模拟框架,但是加入水域流速的概念,让水花可以随波逐流。

那么,为什么要在意这个细节呢?

首先是因为玩家的确会走到河里面。如果我们还是使用不考虑水面流速的模拟方法,会得到这样的效果:



第一反应可能是还不错,但是玩家一会儿就会反应过来,为什么波纹不会被冲走,并且角色站在湍急的水流中,水流也应该要冲到角色身上。

从玩家角度出发,当然希望获得更真实、沉浸的游戏体验。作为游戏开发者,考虑到性能消耗,更希望以最小代价实现并提供更多具有真实感的游戏体验。毕竟我们都希望能对细节有更多的追求。

从抄作业的角度出发,笔者没有发现很多可以参考的案例,欢迎大家可以在评论中补充。在大表哥2 (Red Dead Redemption 2)中,有一些流速对模拟有影响的体现:



游戏中的河道表现

一般来说,游戏中的河道都是由正常水面+flowmap或者流向参数来完成“流动”的渲染。有单一流向的:

荒野大镖客2:



有需要细节流向的:

Cities Skylines:



Uncharted:



也有Unreal自带的:



和我们自研的Photon Water System,支持根据场景几何信息,自动生成流向图(flowmap)用于渲染:



不难发现,虽然渲染方法各有千秋,但是存储流向信息的方法基本都会用到流向图(Flowmap)。比如在PhotonWater中,我们使用的Flowmap方向大致如下图所示:



如果是单一方向的河流,理论上也可以只用单一的方向向量。有了河流的方向,也为我们后续的模拟方法提供了基础。

在浅水方程中加入流速影响

话不多说,先看看应用河流等流速影响之后的模拟效果。



也可以体现角色静止和河道分流处流向不同的细节:



扩充模拟流程

在前文提到的基于浅水方程的水面交互方法中,我们并没有讨论河流的模拟情况,并不是因为浅水方程不支持流速的求解,而是因为我们在上一篇文章中提到的方法进行了尽可能的简化,导致忽略了速度场的求解,只单独求解了压力(高度)场。相反地,比如PhotonWater中的Flowmap求解正是基于浅水方程的,只不过是另外一个形态。

但是在实时的模拟方法中,我们希望尽可能减少性能消耗,所以我们最好避免使用“完整版”的浅水方程求解过程。在本文的实现中,我们使用了单独的Flowmap Advection Pass来给模拟加入流速影响。模拟流程就变成了下图所示。这样的好处是Advection过程非常独立,可以开关。缺点是这确实不是在“求解”速度,只是叠加一个外部的速度场,但是考虑到我们已经做了很多简化,只要能达到可以接受的效果就行。



Advection

在物理模拟中经常用到的词语,我的理解是把模拟的属性(高度/速度/密度等)根据空间关系传递到下一帧。那么在我们的模拟中,该怎么根据Flowmap方向传递高度呢?虽然Advection在很多模拟过程中都有体现,我们试着从最基本出发理解一下。

最直观的想法是,高度图和流向图在二维上是重叠的。所以我们可以把高度图中每一个像素中心的高度根据其对应的速度移动到目的点。如下图所示(图片参考自引用的视频https://www.youtube.com/watch?v=qsYE1wMEMPA):



我们很快会发现一个显而易见的问题,每个像素移动后的位置都很可能不是像素中心,这给我们基于像素中心表示的高度图叠加带来了困难。我们当然可以让移动后的位置根据双线性插值来影响周围4个点,但是叠加过程对于GPU的并行非常不友好。

另一种方法,也是模拟中经常被使用的方法,既以每个像素中心为基点,根据当前速度,得到这一点之前所在的位置(回溯)。如下图所示:



我们根据左上角点速度,如蓝色箭头反向得到的位置,再取周围四个点,由绿色箭头的指向,就可以像Sample贴图一样,双线性插值得到对应的高度。对高度图中每个点做这个操作,就可以完成Advection,因为每个点的操作是独立的,所以可以用GPU很好的并行。伪代码如下:

int2 CurrentIndex=dispatchThreadId.xy;

float2 FlowmapVector=GetFlowmapVec(CurrentIndex);

// Get traced back indices by -FlowmapVector

float2 TraceBackIndex=CurrentIndex-FlowmapVector*FlowmapStrength;

// Handle boundary conditions inside the bilinear sample

OutCurrentHeight[CurrentIndex]=BilinearSampleHeightsAtIndex(TraceBackIndex);

// We need to update history height at the same time because the simulation depends on two frames of history data.

OutHistoryHeight[CurrentIndex]=BilinearSampleHistoryHeightsAtIndex(TraceBackIndex);

对流向场的要求

如果是固定方向的河流,对流向并没有要求。但是如果使用了复杂流向的Flowmap,则对Flowmap的整体流向有一些要求。否则模拟过程很可能会出现不收敛而导致的爆炸。

我们的模拟区域一般是有限的,包括贴图大小也是有限的,所以在有限的模拟区域中,我们要尽量保证区域中整体水量的不变。在上篇文章中,我们的方法有Volume Conservation的介绍,就不赘述。不过加上了Flowmap Advection之后,想要保证整体水量不变,我们需要整个流向场流入的水量和流出的水量是一致的。虽然我们上文的模拟方法加上了一个全局的Damping,但是除非我们要牺牲模拟的真实感,加大Damping,否则随着时间推移,我们的模拟迟早会因为Flowmap区域积累水量大于damping而爆炸。

河流渲染中,一些Flowmap可能是Artists手绘的,那我们该怎么得到一个流入水量和流出水量相等的Flowmap呢?

这个看起来是个很高的要求,但是对于任意的速度场,我们都有两个描述方法,散度(Divergence)和旋度(Curl)。二维中,可以如下图表示:



不难发现,在流体速度场中,Curl表示速度的旋转,Divergence表示水流流入和流出。对于一般的不可压缩流体,我们希望任意的速度场的散度都是0,这代表了没有任何水流入和流出,也就符合了我们模拟的需求。还好我们站在巨人的肩膀上,根据Helmholtz's theorem,任意的的速度场都可以表示成一个0散度和0旋度的场的和。



我们可以根据这个定律求出一个divergence-free的flowmap,这样就可以满足我们的需求了。对于求解过程,因为其实并不是本文的重点,计算也可以离线完成,直接使用计算结果用于渲染和模拟,可以参考这个视频中最后一节https://youtu.be/qsYE1wMEMPA?list=PLi3obdsHfCiIjjfoF43df61WR1aXNhNID&t=699的方法,或者在维基百科https://en.wikipedia.org/wiki/Helmholtz_decomposition了解推导过程。

在PhotonWater中,生成的Flowmap已经完成了这一步,所以不需要额外处理。

边界条件

当根据速度回溯采样像素时,像素位置可能超过边界。边界处理可以参考上文的两种方式,反弹或者消散。对于无限边界的模拟,我们需要像采样贴图一样,Wrap得到高度值,这样才能避免在贴图的边界时,高度信息丢失。

性能参考

作为一个独立的Pass,Flowmap Advection的性能消耗可以非常低。并且在没有流速的湖泊和池塘等,我们可以方便的关闭这个Pass,节省性能开销。

如我们第一篇文章介绍的一样,在模拟分辨率范围内,有一个Flowmap的TextureSample,对当前高度和历史高度进行双线性差值高度图采样。对于固定流向的河流,我们甚至可以省去Flowmap,只用传入一个速度作为参数就可以进一步压缩性能消耗。在2080S上,该Pass GPU耗时大概为0.1ms,但是理论上根据实际应用场景(主要是如何获得水流速度),还有进一步优化空间。

>>参考

1. Games103(https://www.bilibili.com/video/BV12Q4y1S73g/?p=10)-关于Surface Waves(SWE)详细的简化过程。

2. Waterline Pro Plugin(https://www.unrealengine.com/marketplace/en-US/product/waterline)-插件中是纯蓝图实现,并且使用SceneCapture,不过参考了一帧只迭代一次的方法。

3.Kass, Michael, and Gavin Miller. "Rapid, stable fluid dynamics for computer graphics."Proceedings of the 17th annual conference on Computer graphics and interactive techniques. 1990.

4. But How DO Fluid Simulations Work? (https://www.youtube.com/watch?v=qsYE1wMEMPA) 参考了本视频中的Advection部分。

5.3B1B的旋度和散度介绍 (https://www.youtube.com/watch?v=rB83DpBJQsE)

6. PhotonWater的GDC演讲(https://www.youtube.com/watch?v=rB83DpBJQsE),介绍Flowmap的生成等。

上一篇:87万同时在线的《博德之门3》能火,主因不全是《龙与地下城》?|rpg|暗黑破坏神|角色扮演游戏

下一篇:腾讯Q2财报:国际市场游戏营收127亿同比增长19%,保持双位数增速|新游|腾讯游戏|中国手游|第二季度营收

网友评论