【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!
一、美术分析
下面是风格化和写实两种风格的草地效果,看上去是两种不搭边的美术表现形式,但拆解其背后的美术特征和实现原理二者在我们的眼中应该是一样的东西(因为两个都是草),并且进一步延伸,花、灌木、树木等等与草都同属植物,其美术特征也很是相似,实现的原理与方法也是大同小异。因此我想在一篇文章中以草地的渲染为引子,分享一下如何渲染出在风中随波荡漾的植物们。
来源:https://www.artstation.com/artwork/xYkGgr
视频:https://uwa-ducument-img.oss-cn-beijing.aliyuncs.com/Blog/USparkle_TA_Grass/2.mp4
视频来源:https://www.artstation.com/artwork/qQVZqR
植物与风的美术三要素:光影、色彩和动画
1. 光影
光影主要受植物的形与材质影响。
许多新鲜草或者树叶表面有一层膜,给人一种油亮的感觉。当一阵风拂过,它们会摇曳摆动,光滑的树叶在运动中改变光线的反射方向,闪烁、律动的高光给人一种生机盎然的美。现实中我们可以感知到茫茫草原上风的形状、方向、速度,一方面是因为我们看到了草弯曲的方向,另一方面是草面高光组成的形状及其变化。我们常感叹风吹麦浪的美丽,事实上我们眼中的麦浪即是风影响下的高光变化。
草往往连成一片,形状上窄下宽,可由茎、叶、花、果等多种形状组成,因此其阴影的比例基本和其高度与密度相关,即根部最阴暗(中性词),顶部受到光线的充分照射。并且草的形状、密度与运动不同会在彼此身上投射多样、无序、自然、跳动的阴影。
当然,树木的高光和阴影因其大小、形状、运动的不同,有着另外的美。
来源:https://www.artstation.com/artwork/XBgA3a
2. 色彩
每一株花花草草都是独一无二的,它们的形状和色彩有自然留下的痕迹。例如一片草地因为长在一片较干的土地上而比周围的草颜色枯黄,一棵树因为上了年纪看上去没有其它树那么茂密青翠。要想还原自然之真美,我们也应该描绘它们的多姿多彩,但我们不可能为每一株制作单独的模型、绘制单独的贴图。
我们可以通过不同轴向的旋转缩放增加一些形状上的简单差异,并且可以随机地或者依据某种规则混合多种颜色预设。
俯瞰树林颜色的差异
俯瞰树林颜色的差异
3. 动画
植物摆动的动画由风力和植物的受力结构共同影响。
首先风不仅有方向,有大小,还有形状(回想一下初中的知识,风是空气在不同温度和压力下的水平移动。)风的方向和大小我们通过一个向量就可以表示,而风随机多变的形状我们可以用噪音来实现。
感知风的形状
强风过境,我们可以看到片片草地统一的运动形态,但是当风力减小,因为不同草的高矮胖瘦和斜度不同,它们的重心也存在差异,此时在重力与风力的相互作用下,它们会有各不相同的运动。
二、性能预判
茂密广布的草需要大量的模型面片来实现,当这些模型的数量达到一定量级后必然会带来性能问题。下面简单分析一下草以及大多数植物渲染的性能瓶颈节点,并提供对应的解决方案。
1. 合批
每一棵草基本对应一个或多个模型,成千上万的草很容易导致CPU提交渲染命令时拥堵,因此我们必须进行合批处理,关于合批是什么请点击下面的文章链接:《技术美术百人计划-图形5.8合批原理讲解 笔记》。
从上面的图表可以判断,GPU Instancing更适合草地等大面积植物的渲染。其他的合批方式均存在局限性,动态合批只能处理少量顶点,静态合批并不适用于有动画表现的非静态对象,而SRP Batch只能在SRP渲染管线使用。
2. LOD
LOD是游戏中常用的技术,英文Level of Detail的缩写,即细节层次。在计算机图形学中,LOD常用于优化三维模型的渲染性能和内存占用,以提高实时渲染的效率。
LOD技术通过根据观察距离和相机位置等因素,动态地调整模型的细节层次,从而在不同的距离和视野情况下使用合适的模型细节。较远处的模型通常使用较低细节级别的模型,而较近的模型则使用更高细节级别的模型,以在保持可视质量的同时减少计算和渲染的负担。
在较大的自然场景内,植被的分布具有纵深和层次感,因此需要根据观察距离和相机位置等因素显示不同层级的LOD。但植物的LOD方案有些微不同,植物的形状结构有其特点,可以被看作一个包裹的圆柱形,从前后左右环视形状颜色相似(想想你不能分辨哪个面是植物的正面/背面)。因此我们可以把远处的植物换成单张面片,而不是简化坍缩后的模型,但我们需要根据相机方向同步旋转面片,并且实时调整正确的光影效果。
3. OverDraw
OverDraw(过多绘制)是指在图形渲染过程中,对同一像素多次绘制的情况。当一个像素被重复绘制多次时,会浪费计算资源和图形处理器的性能。
OverDraw的常见原因包括:
- 透明物体叠加:当多个透明物体在同一位置并重叠时,将会导致过多的绘制操作。
- 不可见物体绘制:将被其他物体完全遮挡的不可见物体仍然绘制会产生过多的绘制开销。
- 多次渲染相同的像素:当多个物体渲染到同一个像素位置上时,会导致不必要的重复绘制。
- 无效剔除:未对不可见物体进行剔除,导致将不可见的物体也进行了绘制。
想要更为详细的了解可以阅读链接文章:《Unity基础:性能杀手Overdraw详解》
许多植物的茎叶模型都是面片的简单穿插,更为细致的形状结构通过贴图的Alpha通道Mask实现。
上面展示的是草常见的模型与贴图样式。
为了降低OverDraw,我们应该尽量使用不透明的方式渲染植物,使模型尽量与植物实际形状和大小贴合。
4. 补充延伸
除了上面主流的草地制作与渲染流程,用曲面细分+几何着色器可以优化整个草地的制作与渲染,但该方案需要软硬件的支持。
关于曲面细分+几何着色器可以通过下面的文章简单了解:《技术美术百人计划-图形 3.3 曲面细分与几何着色器 大规模草渲染 笔记》
如果想要实现该效果,可以通过下面的视频学习:https://www.youtube.com/watch?v=MeyW_aYE82s&t=918s
用曲面细分+几何着色器渲染草地可以优化掉合批(该方案的草并不是一个个独立的模型,它们与草地是一体的)、LOD(可以根据据摄像机的距离影响草地生成的变量,降低远处草地的密度)、OverDraw(可以直接用代码控制草的形状,避免透明带来的OverDraw)问题。
三、方案拆解
1. 全局颜色
所谓全局颜色就是将所有的草看作一个整体,在全局、整体层面控制草的颜色,而非关注单颗草的色彩表现,实现植物整体和谐自然的美。
既然要实现全局的效果,我们必然想到要在世界空间内做文章,要实现自然随机的效果又想到噪音贴图。
所以,以世界空间XZ坐标采样噪音,以噪音混合植物颜色。
但是,当植物体积较大时,我们单纯的以模型顶点在世界空间中的XZ值取获得对应的颜色,会造成一颗植物身上存在怪异割裂的两种颜色,想象一个人左边是白色皮肤,右边是黑色皮肤那肯定是不自然的。这个问题的解决方法也很简单,我们可以把模型顶点采样改为模型原点在世界空间采样噪音值,这样模型上的所有顶点可以获得了一个统一值,即原点采样得到的值。
UE实现:
Unity实现:
float GlobalMixNoise (float2 WorldXZ, sampler2D NoiseTex, float UVscale){WorldXZ /= UVscale;float noise1 = tex2D (NoiseTex, WorldXZ / 60).r;float noise2 = tex2D (NoiseTex, WorldXZ / 8).r;return noise1 * noise2; }
2. AO效果
对于草地来说,动态阴影的性能消耗很大,因此一般对美术效果影响不大的情况下会关闭阴影效果,并且有动画的草也无法烘焙AO效果。但如果只有光没有影整个画面会不真实,缺少立体感、空间感。
我们知道草的AO来源于草与草之间的光线遮挡,树叶的AO来源于树叶之间的光线遮挡。知道了AO的形成原理,我们就可以大致判断AO出现的位置,进而可以在Shader中通过算法近似地模仿AO效果。
作者的思路是通过距离渐变遮罩加深草靠近根部的颜色,或者加深树冠内部的颜色。
UE实现:
UE提供了可以直接使用的球体遮罩节点。
球体遮罩(SphereMask)表达式根据距离计算来输出遮罩值。如果某一个输出是某个点的位置,而另一输入是具有某半径的球体的中心,那么遮罩值将是0(位于球体外部)和1(位于球体内部),并存在一定的过渡区域。
来源:俞宇原先生
Unity实现:
代码解释:计算顶点至根部的距离,并将其压缩到0-1,作为遮罩得到越接近根部越暗的AO效果。
float SphereMask(float3 position, float3 center, float radius) {float dis = distance(center, position);float mask = smoothstep(radius, 0, dis);return mask; }
上面的方法也许并非最适合你想要的效果,仅仅是一个思路。
例如,实现树叶的AO效果我们甚至可以通过一个菲涅尔算法实现一个不太正确但好看的效果,下面的视频中则使用VectorLength实现效果。
https://www.youtube.com/watch?v=CptpQ5vSy0U
3. 风动效果
对于模型来说一切动画都是顶点的运动,因为点——线——面——形中顶点(Vertex)是最基础的元素。催动顶点运动的方式有很多,骨骼动画(四元数)、形态键(Lerp)、VAT(位置/时间)、顶点动画(Sine等)等等,选择何种方式还是取决于效果。
下面从引擎的角度拆解分析一下风中草(植物)的动画,对引擎来说一切运动/形态的基础是缩放、旋转和平移,在草(植物)的运动中缩放的影响可以忽略不计。草(植物)主要以根部为固定点左右/前后摆动,所以它的运动既可以被看作是以根部为圆心的旋转(旋转的角度受风力大小影响),也可以被看作是从低至高强度递增的位移效果,简言之就是让模型顶点对应风的方向、强度时间连续地旋转或者位移即可。
上面我们简略地从图形学、游戏引擎、数学、生物、物理和动画的角度分析了风中草的动画表现,我们有很多方法可供选择,以实现大致的动画效果。
方法一:
其中,简单的方法是用一个Sine函数在世界空间X、Z方向做随时间的循环位移,并用UV的Y方向值做从高至低的位移强度递减,使根部的位移值保持0,因此也需要对模型的UV做特殊处理保证草模型垂直展开在UV坐标上,并且模型最底部与UV.y=0对齐。具体实现方法可以看下面的链接:《[原神风格渲染02]URP下的草地建模+Shader还原》。
此方法简单,但效果并不一定理想,Sine函数的特性使草的运动有些柔软、周期重复感明显,并且不适合模型更为复杂的植物。但对于结构简单的风格化草地是一个理想方案,并且其柔软的效果也非常适合水中植物的表现。
来源:https://zhuanlan.zhihu.com/p/570419018
方法二:
第二种方法是UE中提供的Wind节点,适合用于许多风效的表现,也包括草等植物。
UE实现:
Unity实现:
float3 Wind(float3 additionalWPO, float3 worldVertPos, float windIntensity, float windSpeed){float speed = _Time.y * 0.1 * windSpeed * -0.5;float3 speedX = float3(1.0, 0.0, 0.0) * speed;speedX = (worldVertPos / 1024) + speedX;speedX = abs(frac(speedX + 0.5) * 2.0 -1.0);speedX = (3.0 - (2.0 * speedX)) * speedX * speedX;float d = dot(float3(1.0, 0.0, 0.0), speedX);float3 speedY = (worldVertPos / 200) + speed;speedY = abs(frac(speedY + 0.5) * 2.0 -1.0);speedY = (3.0 - (2.0 * speedY)) * speedY * speedY;float distanceY = distance(speedY, float3(0.0, 0.0, 0.0));float angle = d + distanceY;float3 point0 = additionalWPO + float3(0.0, -10.0, 0.0);float3 rotatePos = additionalWPO - point0;rotatePos = mul(float3x3(cos(angle), -sin(angle), 0.0,sin(angle), cos(angle), 0.0, 0.0, 0.0, 1.0), rotatePos);rotatePos += point0;return (rotatePos * windIntensity * 0.01) + additionalWPO; }
方法三:
用于实现旋转的四元数也非常适合实现草的旋转。四元数旋转是一种常用于表示和计算三维空间中物体旋转的数学工具。相比欧拉角旋转,它能有效地表示旋转角度和旋转轴,并提供连续、平滑的旋转插值。
四元数由四个实数组成,通常表示为q=(w, x, y, z),其中w是实部,(x, y, z)是虚部,也是旋转向量的坐标。四元数可用于表示物体围绕任意轴旋转的情况。
四元数旋转的基本步骤如下:
- 创建初始的四元数表示旋转。
- 根据旋转轴和旋转角度,计算出四元数的虚部坐标 x, y, z。
- 对四元数进行归一化,确保其长度为1。
- 根据需要,可以通过乘法操作将多个旋转组合起来。
- 如果需要进行插值,可以使用球面插值等技术。
下面的链接详细分析了如何将四元数转换为旋转矩阵:《四元数和旋转(Quaternion & rotation)》
UE实现:
Unity实现:
float3 QuaternionRotate(float3 RotationAxis, float RotationAngle, float3 PivotPoint, float3 points) {// 将旋转角度转换为弧度float radians = RotationAngle * 0.0174533; // 将角度转换为弧度// 标准化旋转轴向量//float3 normalizedAxis = normalize(RotationAxis);// 计算旋转四元数的参数float halfAngle = radians * 0.5;float s = sin(halfAngle);float4 quaternion = float4(RotationAxis * s, cos(halfAngle));// 对点进行平移到旋转中心 float3 translatedPoint = points - PivotPoint;// 计算旋转后的点 float3 rotatedPoint = 2.0 * cross(quaternion.xyz, translatedPoint * quaternion.w + cross(quaternion.xyz, translatedPoint)) + translatedPoint;// 对旋转后的点进行平移回原点 rotatedPoint += PivotPoint;return rotatedPoint; }
方法四:
以上方法适合结构较为简单的植物,对于层次结构复杂的树并不适用。我们可以把树看作是具有多个层级的骨骼,每个层级围绕对应的“关节点”运动,即树的整体围绕树根旋转,一级树枝围绕其与树干的交界点旋转,二级树枝围绕其与一级树枝的交界点旋转,树叶围绕其与树枝的交界点旋转。
来源:俞宇原先生
所以,对于结构复杂的树我们可以使用UE(我们也可以在Unity还原类似的技术)为我们提供的Pivot Painter(Pivot即各层级的交界点,包含位置与方向)技术实现风动效果。
Pivot即各层级的交界点,包含位置与方向
下面的链接详细介绍了UE的Pivot Painter2技术,以及其内置的相关材质节点:
https://docs.unrealengine.com/5.3/zh-CN/painter-tool-2.0-material-functions-in-unreal-engine/
Pivot Painter技术的关键是将Pivot的位置与方向记录在一张贴图上,以供材质读取。其旋转动画的实现仍然以上面的方法三为基础,区别在于需要在Pivot贴图中获得各个模型顶点的旋转原点(Position)与轴向(Direction)。至于如何获得Pivot贴图,Houdini、Blender、3DMax等主流DCC软件都有提供支持,下面分享一些链接以供参考制作Pivot贴图:
Houdini方法:
《沙滩Beachc:Houdini+Pivot Painter2在UE4中制作多边形重组动画》
Blender方法:
《Unreal | Pivot Painter in Blender》
4. 曲率控制
我们为什么要给植物的风效动画加上曲率?
因为,物体的弹性和重量感对于动画效果至关重要。通过变化形状的方式或者模拟物体受力的效果,可以使动画中的物体看起来更符合现实世界的物理规律。
下图分别是无曲率旋转和有曲率旋转的区别,可以看出曲率的加入对于效果的影响巨大,没有曲率的竹子看上去非常僵硬。
来源:俞宇原先生
来源:俞宇原先生
曲率的实现使用了和AO效果相同的思路——球体遮罩(SphereMask)。
5. 风浪高光
我们在前面的美术分析中详细阐述了风浪与草高光的对应关系,高光对于表现麦浪的动态与光影之美至关重要,但是我们无法直接在光照模型中直接获得正确的高光效果。
一方面是性能原因,与阴影类似,随着顶点的位置变化我们需要动态的修改法线方向才能获得正确的光照。另一方面是为了优化植物的光影效果(下面第7点将展开解释)我们会修改模型原本正确的法线方向,所以我们的高光效果会受到影响。
所以我们此时只能脱离光照模型的思维局限,学会用错误的算法获得正确的效果。
我们的做法是,根据草高光出现的位置用顶点色制作遮罩,结合风效噪声图动态提高颜色亮度(因为高光需要与风浪同步),并与草原本的颜色做混合,如果我们认为高光的亮度不够甚至可以把高光颜色传递给自发光,得到更好看的效果。
6. 角色交互
角色交互本质也是一种草(植物)的动画效果,区别在于我们需要加上角色位置与方向对于动画的实时影响。因为我们在风动效果中提供了四种方法,每种方法也需要匹配对应的角色交互方案。
方案一:
根据模型距离玩家的距离与方向计算模型的旋转向量,实现角色交互带来的不同方向与强度的旋转效果。适合使用了四元数旋转矩阵的风动效果方法三与方法四。
Unity代码:
float3 playerVector = normalize(_ActorPos - pivotPoint);playerVector = normalize(cross(playerVector, xvector));float actorMask = SphereMask(_ActorPos, pivotPoint, _ActorSphere);xvector = actorMask > 0.5 ? playerVector : xvector;//把xvector传递给函数//float3 QuaternionRotate(float3 RotationAxis, float RotationAngle, float3 PivotPoint, float3 points)//的RotationAxis参数。
方案二与方案三:
上图提供了方案二与方案三,方案二需要UE距离场功能支持,方案三需要输入一个玩家位置数据,但算法与方案一略有不同。方案二与方案三的本质是获得一个角色交互范围的遮罩,然后用这个遮罩影响风效,但方案三不受距离场功能的限制可以用于更多场景。
7. 光影优化
在真正的植被渲染中我们往往使用包裹式法线,或者在一些面片制作的草模型中把所有法线方向更改为(0,1,0)(对应xyz,即法线方向指向上方),这样做的目的是为了使植物的光影效果整体整洁。
来源:@SakuraWhite
来源:@Nanyin
这么做的原因是植物模型,不论是草还是树,大多是平坦的面片穿插而成。如果保持使用单个面片原本的法线,当我们从整体的角度看这个模型时模型的法线将非常凌乱且不符合自然规律,进而光照模型得到的光影效果也会很丑。就像我们画画时应该关注整体的画面结构、光影和色彩,而不去过分在意某处细节的正确与否,植物的法线也应该从整个模型的角度观察。
Face Normal:
来源:https://www.youtube.com/watch?v=E_IiqijpfS0
Bent Normals:
来源:https://www.youtube.com/watch?v=E_IiqijpfS0
Object Normal:
来源:https://www.youtube.com/watch?v=E_IiqijpfS0
Camera Normal:
来源:https://www.youtube.com/watch?v=E_IiqijpfS0
下面分享两个在DCC软件中修改植物法线的方法,感兴趣的可以阅读一下:
《Blender 中制作风格化植被的包裹式法线》
《Houdini在游戏里的简单运用p3(插片草从)》
这是侑虎科技第1648篇文章,感谢作者RE Ding供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)
作者主页:https://www.zhihu.com/people/ding-yan-qing-75
再次感谢RE Ding的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)