渲染TA实战:模型草美术效果分享

Hi!大家好我是小圆,来自畅游引擎部TA组。这次分享的内容是模型草的美术实现,包括草的模型和shader以及renderer Feature的制作。最后实现的效果:简单的风格化的光照效果、草随风摇摆、和草的稳定碰撞交互、以及和草带有弹力的碰撞交互。

视频链接:https://zhuanlan.zhihu.com/p/609035535

↑这是草的Shader面板的一些可调属性

视频链接:https://zhuanlan.zhihu.com/p/609035535

↑这是配合Renderer Feature做出的草的碰撞效果,一种更加稳定便宜,另外一种富有弹性。

特别提前说明说,本篇分享主要关注在草的动态效果,包括风拂动、和角色的交互。草的大规模生成、存储、实例化渲染不在分享的主要关注范围内(因为有同事做了很棒的GPU驱动的大规模草地渲染,就不用我来做了哈哈哈手动狗头)

那我们开始进入正文。

从做一簇草的模型开始

做一簇草,首先我们需要几棵长短不一的草作为基础。草的根部因为晃动幅度相对尖端幅度较小,可以分段略宽,在草中段和尖端要分段要做的稍微细致一些。同时我们也可以为单独的草做好LOD,方便后续LOD的制作。我这里分了5级LOD,LOD3和4里,最短的那一根草我直接删掉了,较小的草在远距离下也看不到。这里的草可以略微带一点弧度,但弧度不要过大。不然后续在计算草的受力效果时会看起来拉伸严重。

有了基本的草面片,我们就可以手动随机的旋转、缩放我们做好的单棵草的模型,就能获得一簇看起来还行的草。摆放我们的草的时候,需要注意:

  1. 高中低三种不同长度的草在缩放的时候,不要串了高度顺序。尤其低矮的草放太大,分段不够的时候会看起来比较丑陋
  2. 不同的LOD级别下,想同的一棵草位置一定要对齐。高级别的LOD可以适当删掉一些低矮的、细小的草。
  3. 一簇草我们需要确定它生长在一定的范围里,比如我这里所有的草都生长在一片2m x 2m的范围内。这个数据之后的Shader里会需要使用。

有一些教程里有草的模型的另外一种制作方式是:所有的基础草面片都直指向天,没有弧度。后续的弯折和倾斜完全靠Shader来控制。这样做确实也可以,但个人觉得这样做会稍微难控制一些,所以直接在一簇草的模型里做一些预设的、不太大的弯曲和倾斜。

有了基础的草的模型之后,我们还需要对草的UV进行一些额外的操作。可能这些操作现在看起来莫名其妙或者解释不太直观,但这对于后续的计算很有必要:

  • 为草添加一个UVW编辑器,在UV0里(max里显示贴图通道1),全选面片、从任意一个侧面进行平面投射。塌陷掉修改器。做好之后的UV0应该与下图类似。
  • 在场景中制作一块能完全覆盖所有草根部的面片,这里我的面片大小是2m x 2m,和我之前种草的范围一致。同时选中他们并为他们添加UVW编辑器,切换到UV1(max里显示贴图通道2),使用平面投射从Z方向(顶视角)进行投射。
  • 然后我们需要将每一棵草的每一个UV点,都挪到这一棵根部的UV位置上去。最后实现的效果应该是:每一棵草的所有UV点,都在同一个位置,且这个位置是草原先根部UV所在的位置。做好后的UV应该类似下图。完事之后就可以塌陷掉这个修改器,删掉我们辅助用的面片了。
  • 我们需要对我们所有LOD进行同样的上述操作

上边这一通操作下来,怕是要瞎了不少模型大哥的眼睛,尤其对齐UV1里每棵草的UV到根部,简直不要太烦人。如果能写个脚本那真是…诶?好像我已经写了一个?

好的,至此,我们的草的模型就已经制作完毕了。

这里简单解释一下UV0和UV1的用途:

UV0主要记录了每一棵草的相对高度,且整体高度被缩放在了0-1之间。这样我们可以在shader里比较容易获取到草的尖端和根部。如果要非常严格的计算草的尖端和根部,也可以对每一根草进行投影、将每一根草的高度缩放到0-1。

UV1里记录了每一棵草根部的位置,之后我们在Shader里计算草所受的力时,需要使用UV1里记录的数据。这样可以避免同一棵草的不同位置,受到不同方向的力,导致错误的拉伸。

02-基础的颜色、和风拂动的效果

模型之后来看看Shader的基础部分。

首先是草的颜色,可以定义三个Color和三个Position,然后根据草的UV0里的y值来插值。颜色的a值我用做了Smoothness。

↑Properties

↑简单的采样

↑最简单的三色渐变

然后是法线部分,我们目前没有在DCC里对草的法线进行处理,草的法线还是默认和表面垂直。这样的效果对于写实风格的草来说或许可行,但对于风格化的草来说就过于杂乱了。在Vertex阶段我们可以将草的法线修改为全部指向天空(float3(0, 1, 0))。

这里扭转法线的操作是在模型空间进行的(实际上我直接传了float3(0, 1, 0)进去,替换了原来attribute.normal)。法线部分的修改后续还会进一步调整,我们暂且先放在这里。

↑目前的效果

然后我们来看草的受力部分。在演示的示例中,草受到两种力:一个平移的噪声图模拟的风力、和场景内碰撞体产生的推力。为了正确的计算受力,我们需要先算出一个用于计算受力的世界空间坐标。记得我们在制作模型的时候,UV1里存的是每根草的根部的位置信息吗?我们在顶点着色器里先把它还原到模型空间坐标,再转换到世界空间里去。这样每一根草的所有顶点,在世界空间里将会拥有同一个坐标。用这个坐标来计算受力可以幼小的避免模型的过渡拉伸。

然后我们就可以用这个世界空间坐标来采样一张随机的法线噪声了。演示这里的噪声是用Substance Designer里的Cloud噪音、用不同种子填充了RGB三个通道,实际上我们只会用两个通道,另外的通道是给别的shader用的。

采样前用内置的_Time来对UV做平移,然后再对贴图进行采样、缩放到-1到1。这时候我们的风力效果还是完全随机的状态,可以给它加上贴图平移方向的力,来模拟持续的、同方向的风力。这里要注意贴图平移方向和风力方向是一致的、和UV平移方向相反。

到这里,草的随风摇摆,在世界空间位置相近的位置,受力也非常接近,为了让每一棵草都有自己的个性,我们可以用之前计算出的草的位置,求得一个随机的0-1之间的小数。乘在我们之前采样好的平移风力图上。得到风吹拂的效果。

↑使用位置随机0-1的debug效果。使用位置和一个向量点积再乘一个很大的数后截取小数部分,一个经典的伪随机算法

我们的草在收到风力摇摆的时候,顶点在水平方向移动的时候,也会在竖直方向上移动。如果要准确的计算草的弹性形变的话,仅仅在顶点着色器里是很难做到的。这里我们使用一种近似的计算:计算每个草的顶点到垂直于地面的线段,在顶端受横向力开始围绕与地面交点旋转时,下降的高度。

然后就可以把风力对顶点的偏移,加进顶点的世界空间坐标了。这里需要注意,所有的位置偏移在靠近草根部分位置都是逐渐变小的,我们直接把模型空间的y坐标乘在偏移的力上。

↑这里的force.xy其实之前的Wind2D

另外这个时候我们可以对法线做进一步的修改了,因为有风力的加入,我们可以将风场的力加入到对法线的影响里。

↑将float3(0,1,0)和草的顶点偏移求加权平均。1.5是个经验值,可以在GUI上暴露给美术调整。

视频链接:https://zhuanlan.zhihu.com/p/609035535

3- 碰撞交互

计算和物体的交互,核心要点是将物体的碰撞信息传递给草的shader。我们案例里的方法是:使用一个略大于角色(在案例里是个球)的盘型模型,将它的法线通过Render Feature渲染在一张RT上,在草计算受力的时候,再去读取这张RT,和风力结合在一起输出到顶点位移上。

这样做的好处在于:首先,相比于以往传递参数给Shader来计算碰撞的物体数量不受限制。如果在Shader里申明了固定数量的位置、大小等信息来计算碰撞,如果实际需要计算碰撞的参数少于申明的参数则会浪费,多了就会有丢失。另外,我们可以通过控制我们的盘状模型的大小、形状、法线朝向,来模拟出草和草之间互相挤压的效果。

↑受到压力的草会向周围专递压力,用一个受力的代理网格来产生推力,可以避免复杂的迭代计算

来看看我们Render Feature的核心逻辑:首先,定义好正交相机和RT的参数,在Configure()函数里,找到打了Player Tag的GameObject,并以它为中心,调整我们的正交相机、并获取正交相机的透视和投影矩阵。

Execute()里,使用ShaderTag和LayerMask来限制我们的碰撞模型只渲染在我们申请的这一张RT上。使用Context.DrawRenderers()提交我们的渲染命令。

最后,如果申请了Temporary Render Texture,一定记得要释放。

Create()和AddRenderPass()算是常规操作,就不再赘述了。碰撞模型所使用的shader也非常的简单。使用从Feature传出的矩阵做投影变换。然后输出法线。就是记得Shader的Tag和Feature里定义的保持一致。碰撞模型的GameObject碰撞物体的Layer也要设置准确。

如果feature设置成功的话,现在使用FrameDebugger应该可以看到我们已经渲了一张RT出来了。

然后我们回到草的shader部分。我们可以使用此前在Feature里传递出来的相机参数,还原出采样RT所用的UV。

采样之后,我们使用保存在b通道的深度信息和相机的信息,还原出碰撞模型的世界空间高度。我们可以用它和草顶点的相对距离,来计算RT对草推力的影响。

现在我们就已经获得了一个可以交互的草。

视频见链接:https://zhuanlan.zhihu.com/p/609035535

除此之外还有一些额外的风格化处理:我们让被碰撞体压到的草缩小一些。否则在茂密的草丛里,有碰撞体压到草丛会让受力的草戳进旁边的草丛里,看起来会稍显凌乱。

视频见链接:https://zhuanlan.zhihu.com/p/609035535

4-让草的交互富有弹性

到现在我们的草应该能满足一些交互的要求了,但是如果要更近一步,想要草看上去更“物理”、有弹性,我们需要对生成RT的步骤做进一步的处理:

草的受力计算算是中学物理的知识,比较简单就不再赘述。具体在Feature里实现的时候需要注意的一点是:其中计算当前帧的速度、和当前帧草的顶点位移时,都需要用到上一帧保存的速度和位移(上一帧碰撞模型的推力可以通过相机的一帧之内的位移推算出来,所以不需要跨帧保存它)。但是TemporaryRT在每帧结束都会执行FrameCleanUp,所以速度和最后的位移这两张RT需要申请普通的RenderTexture。

↑这里的Blit步骤中,Velocity_Temp在使用完之后应该可以继续用在FinalTmpde的位置。这里为了过程看起来清晰就不做修改了。

视频见链接:https://zhuanlan.zhihu.com/p/609035535

欢迎加入我们!

感兴趣的同学可以投递简历至:CYouEngine@cyou-inc.com

#搜狐畅游##引擎开发工程师##2024届秋招#
全部评论
这么干货发牛客
点赞 回复 分享
发布于 2023-08-25 17:21 江苏

相关推荐

6 5 评论
分享
牛客网
牛客企业服务