渲染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 江苏

相关推荐

秋招结束已经一段时间了 一直在忙着毕业的事情 浅浅总结一下自己的秋招经历吧~本人BG双非硕 后端选手 有一段小厂+腾讯暑期实习腾讯暑期转正loser秋招结束已经结束了有一段时间了总结一下秋招历程最大的感受就是秋招比起暑期更加卡学历秋招总共投了60多家吧一直面 一直挂也投了两家银行科技岗 都走到终面体检了都拒了(总体感觉本地的银行还是挺容易过的)可能本人更想去私企 并且银行也挺卷听说一直到11月就只有一家小厂的offer并签约当保底然后也突然被WXG捞了 本来都不对腾讯抱有希望了可能经过一整个秋招的面试积累吧 以及本人有ACM经历 WXG整体面试以做题偏多(一二面做了5道题 4道hard) 比较合自己胃口 差不多半个月就把五轮面试过了进入录用评估 但也一直没有结果到后面也陆陆续续有几家中厂也终面过泡池子一直到12月初华子给开了base杭州 14a因为华子公积金的原因 和小厂薪资上差距不大 所以也一直犹豫是否毁约签华子 但是内心也还对WXG抱有一丝幻想(虽然一直没有保温也没有任何消息)然后一直到12月中下旬 华子要求去现场签约了 但是WXG还是没有消息 然后就连续发邮件和打电话催了好多次 还是回复耐心等待直到华子签约那天 经过内心挣扎已经决定毁约签华子了 可能还是想平台更大一点吧 然后最戏剧性的一幕来了 就在我发毁约邮件没有5秒 WXG打电话开奖了 并且开奖也十分有诚意 最终还是没有签约成功华子 研究生期间也打了很多次华子的比赛还是对华子有感情的555整个秋招都是伴随着焦虑的 我认为自己也是秋招大部分人的画像 屡屡碰壁后不断怀疑自己 但是可能自己也比较幸运吧 但是也感谢自己在一次次陷入迷茫都没有放弃自己 还是一直努力背八股 刷题也祝各位牛友们共勉 就算暂时没有好的offer 不放弃一定会有好的结果的!!
点赞 评论 收藏
分享
评论
6
5
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务