游戏开发论坛

 找回密码
 立即注册
搜索
查看: 2703|回复: 0

塞尔达风之杖渲染解析

[复制链接]

1万

主题

1万

帖子

3万

积分

论坛元老

Rank: 8Rank: 8

积分
36572
发表于 2019-12-24 12:21:38 | 显示全部楼层 |阅读模式
1.jpg




1 (1).gif


2.gif


通过线框绘制模式可以看出“雾”是由几个缓慢移动的自旋透明片构成(始终绑定在摄像机上),很寻常的做法。去掉纹理过滤后会更容易看出来。
3 (1).gif




4 (1).gif


5.gif


云实际实际上是Billboard片,出现时有缩放和透明度变化做过渡。

但是这样做只能显示云的侧面贴图,无法处理云在头顶的情况。所以游戏里视角通常是不能朝上看的。

但在第一人称视角时必须允许朝上看,所以它做了以下处理:

  • 所有Billboard片的方向并不是固定朝上,而是朝向头顶中心点。朝上看的时候,所有云都指向天空中心,这样就不会出现视野旋转而云不转的情况。
  • 但云在接近天空中心的时候朝向不对的问题依然不能解决,而且通过中心的时候还会旋转180度。所以它选择让云接近中心时执行消隐逻辑,任何时候头顶区域注定没有云存在(即使是暴雨天气)。

6 (1).gif


7.gif




8 (1).gif


9.gif



众所周知,点光源的渲染成本远高于平行光。

风之杖的所有点光源都能照亮场景,并体现出点光源的球形范围特征,更重要的是,它并没有灯光数量上限(第二张图两个灯光区域叠加了)。

虽然点光源说起来也就是逐像素计算和灯光的距离,算一次向量自点乘,但在场景光源较多的时候,剔除掉范围外的光源也需要很多计算——而且实际上是做不到的,因为塞尔达每个Renderer都很大,只有不同Renderer才能选择不同的光源组,然后又不可能用延迟渲染或者Tile-base按屏幕范围拆分。

风之杖用的是一个很hack的做法。它的每个光源都是一个多边形的球体。


2.jpg


然后设成Cull Front(正面剔除),以及ZTest Creater(被遮挡时才显示),就成了这样:

3.jpg


所以它的光照投影才不是个正圆,而是个微妙的Low Poly风格多边形。这并不是故意而为之,单纯是不想给这个光照球太多顶点。这样做并不用专门剔除光照,也不会有任何额外绘制,但表现力就到此为止了,最多也就是用多个不同大小的光照球叠加来制照一些“光照边缘”,因为在光照球的shader里其实是不知道墙面和球的交点在哪的。

4.jpg

3个不同大小的光照球叠加


但是如果有不透明物体的深度缓存,就能在屏幕上求出其交点,换算回世界坐标并和光照中心求距离,并算出正确的点光源光照来。而这其实就是延迟渲染中对光照部分的处理,所以现在看也不完全是个hack的技术了。

此外,人物的光照和投影为求效率也使用的是平行光。对于多光源的处理,它采取的是接近后“捕获”的做法,在公共区域用的则是最后一个接触的光源,在光源切换的时候是直接对两个光源的位置做插值,以避免切换时的突变。(请仔细观察人物投影方向和光照方向变化的瞬间)

10.gif


11.gif


12.gif




罐子和金币等物体的影子采用了常见的透明圆片的设计(角度由下方碰撞箱的法线决定),所以可以看到这个明显的显示错误:

5.jpg


但是从上面看的时候,超出平台的阴影却能被隐藏。

6.jpg


由下图隐藏了一部分的阴影可以判定,这个阴影实际上是通过Offest将自身的深度检测向后位移了一段距离,然后设定为ZTest Greater(被遮挡)才允许显示。这是个相当“实用”的贴花技巧。

7.jpg


至于动态阴影部分,

8.jpg


9.jpg


10.jpg


11.jpg


个人认为是每个人物单独生成ShadowMap,然后从脚底附近的简化Mesh碰撞箱筛选三角形动态创建Receiver(依据是与人的距离,法线与光照方向的夹角,所以从崖上往下的阴影无法在悬崖上显示出来)。

因为是每个人物单独生成ShadowMap,每张贴图都可以比较小,内存占用较小。但就算只渲染一个人,这张贴图依然精度极低,锯齿不明显是对边缘半透部分step的结果。

12.jpg


最终锯齿部分近似为斜线


这个游戏还有个特殊的设计:一旦人物悬空,或者在梯子和挂边的时候,阴影就会固定到人物正下方。个人觉得这是为了表示人物的落点位置,单纯是为了游戏性考虑。但这样的设计确实也方便了Receiver的筛选,远处的物体将永远不可能被筛选到,而且也避免了ShadowMap精度低,拉长后过于明显的纹理走样问题。

13.jpg






13.gif


相比大片纹理产生的overdraw,大量粒子生成的多边形更加合算。雨实际上是由大量狭窄雨点三角面渲染而成的。

但由于雨点的下落速度一致,并不需要每个雨点单独作为粒子,将十几个雨点绑定成一个整体,随机水平位置生成向下位移即可。

14.jpg


除了雨点本身外,还需要生成落在地面溅起的水花。用物理检测实现是不可能的。通过反复观察水花生成的“错误”,风之杖的水花应该只是简单地在低于视角的范围随机生成(高于视角看到的是Mesh背面,即使生成了也看不到),但是用的是ZTest Greater,也就是只有当水花被建筑遮挡的时候才会显示。这样水花就不会出现在空中。

但是当地面高低差较大的时候,由于水花范围没有那么广,无法落在建筑下方,就无法显示出来。

此外,这也会错误地导致墙面上出现水花,但因为墙壁的水花和视线几乎平行,是一个极扁的圆,错误生成了也不是很明显。

气流扭曲

14.gif


风之杖中广泛使用了这个效果。做法很常规:在绘制完其他全部物体后,Grab Pass,最后绘制扭曲部分(不用Grab Pass直接取上一帧图像会采样扭曲效果本身,导致越偏越远)

15.gif


16.gif


扭曲效果的本体是一个普通的半透圆片贴图粒子,除了透明部分外,色彩部分直接用的Grab Pass的贴图再加一个噪波贴图位移产生扭曲。并没有什么可说的。

15.jpg


16.jpg


但是在一些特殊情况下,“全屏”+“高速”,它则用了一个更近似的方案

17.gif


GrabPass还是依旧,但这个效果并没有用噪波纹理,甚至没有用透明纹理,而是疯狂甩出了大量高速移动的带Grab贴图的不透明粒子。

18.gif

偏移摄像机后



取一帧查看

17.jpg


它只是稍微偏移了下采样的坐标,然后靠不透明片随机叠加,单帧的效果其实有一定破绽,但在连续画面中是看不出来的。而因为不透明物体重叠会正常pixel kill,性能不差。

非要这样做而不是用正常的后处理,应该主要还是为了体现出气流运动的方向,用噪波的效果未必比这个好。运算量也确实要少一些,就像上图的情况,其实有一部分屏幕根本没绘制,而后处理至少要绘制一屏。省掉的噪波纹理那方面就更不用说了。

岩浆

19.gif


这个岩浆效果的特点是:有实际高度变化,且足够随机(并且符合岩浆的特征)

有经验的人很快能看出,岩浆的颜色=水面高度(取世界坐标Z作为依据lerp),不需要纹理。剩下的问题就是如何生成这样的一个岩浆高度场。

正弦波叠出来的都比较接近波浪,而岩浆池的波形特点则是“沸腾”,简而言之是个混沌系统,这是简单的周期波模拟不出来的。虽然FFT理论上可能能模拟出来(但这就扯远了)

实际上是这样的:

20.gif


看不明白就再放一个比较“稀疏”的例子

21.gif


22 (1).gif


说白了,还是随机“粒子”。

每个粒子是个两层的8边形,中间凸起。

18.jpg


随机位置生成粒子后,从水平面下凸起来,然后再沉回去,作一个周期,然后移动到下一个随机地点再次出现。

粒子依然是不透明物体以减少OverDraw,因为颜色是由高度决定的并不需要半透过度,产生和消失都在“水面”以下,也不需要半透渐隐。

叠加得越多,效果越自然。

顶点数看上去很夸张,但也只是看上去而已(线段长,交叉多)。实际上一个粒子也就16个顶点,而一个湖有50个粒子效果就已经不错了,随便一个水面需要的顶点数其实很轻松就能超过它。

效率问题反而在OverDraw上,虽然是不透明物体,依然还是有概率重复计算的。按Z排序后能缓解,但边界处依然存在。

不过基本没啥问题的,毕竟风之杖只是个NGC游戏,这个能跑还有啥不能跑。

很容易联想到,是否普通的水体,或者海是否也能用类似的方法实现……

理论上是可以的,只是需要处理法线连续,所以必须先把高度场渲染到纹理处理,然后再映射到网格上。

神海4用到的Wave Particles听说就是类似的技术。

水体

水的部分内容较多,分成多个小节描述

(一)边缘

水是透明的,人对水的感知更多依附于水与其他物体的交界面。

23.gif


这个游戏中大部分水都是一张纯蓝色的贴图,然后在边缘处单独拉一个Renderer,使用特殊材质,拉好UV。

19.jpg


使用两张贴图,4次采样。同种贴图UV展开方向是相反的,相加后saturate,这样出来的纹理看上去并没有那么重复。

20.jpg


白沫

21.jpg

深色倒影


(二)海浪拍岸

24 (1).gif


这里取了个巧,任何位置的拍岸节奏都是一样,只有UV x轴的步进速度不同。

海岸分3层,由下至上是:

1.波浪退去时潮湿的部分:

22.jpg


UV并不是位移,而是直接贴边对y轴缩放,y轴像素拉伸产生的模糊模拟了湿边的扩散。

2.有实体的海浪,在图示范围正弦摆动,一个完整周期要摆动两次。

23.jpg


24.jpg


这个波还有个不易察觉的细节:

25.jpg


接近岸边倒极限的时候会显示一个水面纹理,二次采样,相反方向运动



26.jpg


使用的纹理


除了提供细节外,还提供了一点软边缘的效果:接近岸边的部分变亮了,更像沙滩的颜色。

3.从深处来的浪花:看着像海浪,其实只有白沫,看着同时存在两个波,其实是通过repeat方式复制出来的。

27.jpg


这个浪花一直保持匀速推进。

整个设计巧妙的地方在于,两个波有一段时间是重合的。而第二种波的频率比第三种大,它推到最内侧之后,还会时间回来,再去接下一个波。重合期间由于一个是匀速,一个是正弦波,所以会错开一点,使得白沫的变化更多。

正因为这个波只有白沫没有蓝色的海水部分,才能和下层的白沫重合在一起。

不过回退的时候处理不好就会出现这个状况:

28.jpg


波2回退的时候,波3还没有完全消失

波3的覆盖范围需要比波2稍短一些。波3的白沫需要在波2回退之前滚动出网格外。

(三)海平面交界线

29.jpg


这是不太容易注意到但是非常重要的细节。海水靠近地平面的地方应该变亮,并逐渐过渡到接近天空的颜色,也就是一种类似菲涅尔的现象。

我们肯定不愿意在frag上正常算一次viewDir点乘法线。但这整个海只是一个Quad,也没法交给顶点去算,那怎么才能让远处的颜色发生变化呢?

用远景雾确实可以,但这还是要算。

30.jpg


调整镜头后,我们可以俯瞰整个海的Quad的形状。可以看到这个Quad并不是纯色,并且其图像在任何方向观察都一样,所以它很可能是贴了个这样的贴图:

31.jpg


放大到足够大,以接近水平的角度观察,视觉上就产生了这样的效果。

32.jpg


33.jpg


(四)波浪

在大陆附近,海面是由一个Quad组成的,这多半是为了性能。为了表现海浪,风之杖就搞了个Billbroad粒子糊弄事。

25 (1).gif


34.jpg


一眼就能看出来简单实现方法,却不得不说效果还成。

但是一旦进入深海,大陆的资源被释放掉后,就会换成一个正规的海浪模拟。

26 (1).gif


28.gif


没有用尖波(Gerstner),海浪也不大,应该就是两个不同频率的正弦波的叠加,在顶点上直接计算。

海浪模拟为了节约计算成本,按说会做LOD,不过移动摄像机后发现LOD层级是一样的,只是到最远处变回了最初的Quad海面。



为了方便视域裁剪按说要对海面分块,规则是近处分块密,远处分块稀疏,这样才能在满足裁剪的情况下减少DrawCall。

37.jpg


不过它LOD都没做,估计就是最大粒度按扇形随便分了下下吧。

另外这游戏整个海面是跟着主角一起移动的(意外地破绽不大),不需要动态创建销毁,其实很容易做分块。

进入深海后海面也有了纹理

38.jpg


两次采样,第二次UV翻倍,而且染成黑色纹理。同时还要做一次随机扰波增加波纹细节。

39.jpg


越远纹理则越淡。

(五)船只尾迹

其实就是个随时间变宽的MeshTail,但是带了一个偏移每一段UV的正弦摆动效果(两个不同频率的正弦波叠加)

29.gif


30.gif


贴图:

40.jpg



作者:flashyiyi
专栏地址:https://zhuanlan.zhihu.com/p/34960962

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2024-12-22 20:38

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表