查看: 50|回复: 0

[简易教程] Unity基础教程系列(新)(七)——有机品种

[复制链接]

724

主题

76

听众

7698

积分

头头

Rank: 12Rank: 12Rank: 12

发表于 2021-2-19 05:36 |显示全部楼层
200+篇教程总入口,欢迎收藏:
本系列合集
本文主要内容:

1、根据深度为分形着色
2、应用基于随机序列的变体
3、让叶子看起来不同
4、使分形像受重力一样下垂
5、增加旋转的多样性,时而可以翻转
这是关于学习使用Unity的基础知识的系列教程中的第七篇。在其中我们会调整分形,使其最终看起来比数字化的结果更自然。
本教程是CatLikeCoding系列的一部分,原文地址见文章底部。
本教程使用Unity 2019.4.18f1制作。
分形经过修饰后看起来很自然
1 颜色渐变
我们在上一教程中创建的分形显然是应用数字化的结果。它看起来僵化,但精确,正式和统一。同时,它看起来也不像是自然的,活的。但是,通过一些更改,我们可以使数字化的结果看起来更自然。为此,我们引入了多样性和明显的随机性,并模拟了一些有机行为。
使分形更多样化的最直接方法是用一定范围的颜色替换其均匀颜色,而最简单的方法是将其基于每个绘制实例的层级。
1.1 覆盖颜色
我们的DRP表面着色器具有_Color属性,目前可以通过调整材质来配置_Color属性,但也可以通过代码覆盖它。为此,请在Fractal中跟踪其标识符。
然后在Update内绘制循环中的属性块上调用SetColor。我们首先将颜色设置为白色,再乘以当前循环迭代器值除以缓冲区长度减一。这会让第一级为黑色,最后一级为白色。
为了给所有中间级别一个灰色阴影,它需要是浮点除法,而不是没有小数部分的整数除法。我们可以通过将除数中的-1变为浮点减法来确保这一点。然后,其余的计算也将变为浮点数。
为了使此功能也适用于URP着色器图,我们需要确保它在内部将_Color用作反照率。内部属性名称会在着色器图中作为blackboard上的属性引用名称而公开。
Albedo内部称为_Color
结果会是一个灰度分形,DRP和URP的根实例为黑色,叶实例的白色。
灰度化的渐变分形
请注意,除数要减一,才能在最深处达到白色。但是,如果分形的深度设置为1,这将导致除零,从而导致无效的颜色。为了避免这种情况,我们应该将最小深度增加到2。
1.2 颜色间插值
我们不会限制于灰度或单色渐变。通过调用具有两种颜色的静态Color.Lerp方法和我们先前用作其插值器的系数,可以在任意两种颜色之间进行插值。这样,我们可以在Update中创建任何两种颜色的渐变,例如从黄色到红色。
黄-红渐变分形
1.3 可配置的渐变
我们可以更进一步,支持任意的渐变,可以有两种以上配置的颜色,也可以有不均匀的分布。这是依靠Unity的Gradient类型来实现的。使用它为Fractal添加一个可配置的Gradient。
Gradient 属性,设置为白-红-黑
要使用渐变,请使用相同的插值器值,用对渐变的Evaluate替换Update中对Color.Lerp的调用。
可配置的 白-红-黑 渐变分形
2 任意颜色
具有渐变色的分形看起来比具有均匀颜色的分形更有趣,但其着色显然是公式化的。有机物的颜色通常是随机的,或者看起来是随机的。对于我们的分形来说,这意味着各个网格实例应显示各种颜色。
2.1 Color Shader Function
为了让表面着色器和着色器图形同时完成工作,我们将通过FractalGPU HLSL文件提供实例颜色。首先在其中声明_Color属性字段,然后是仅返回该字段的GetFractalColor函数。将其放在shader graph functions上方。
然后从表面着色器中移除现在的多余的属性,并在configuresface中调用GetFractalColor,而不是直接访问字段。
由于我们不再依赖材质检查器来配置反照率,因此我们也可以将其从Properties块中删除。
通过将输出参数添加到为其创建的着色器图函数中,我们将向shader graph暴露分形颜色。
在shader graph本身中,我们首先需要删除Albedo属性。可以通过右键菜单将其删除,方法是右键单击Blackboard上的标签以将其打开。
只有smoothness属性
然后将Output添加到我们的自定义函数节点。
自定义函数的额外FractalColor输出
最后将新输出连接到主反照率上。
使用FractalColor作为反照率
2.2 基于实例标识符的颜色
为了实施每个实例的多样性,我们需要以某种方式使GetFractalColor取决于所绘制对象的实例标识符。由于这是一个从零开始增加的整数,因此最简单的测试将类似于返回按比例缩小三个数量级的实例标识符,从而导致灰度渐变。
但是现在,我们还需要确保仅对启用了过程实例化的着色器变体访问实例标识符,就像在ConfigureProcedural中所做的那样。
在这种情况下,不同之处在于我们总是必须返回某些内容,即使那可能没有多大意义。因此,我们将简单地为非实例化着色器变体返回配置的颜色。这是通过在#endif之前插入#else指令并在两者之间返回颜色来完成的。
通过实例标识符进行上色
这说明该方法有效,但看起来也很糟糕。我们可以通过(例如)每五个实例重复一次使渐变变得有意义。为此,我们将通过%运算符使用模数为5的实例标识符。这样会将标识符序列转换为重复序列0、1、2、3、4、0、1、2、3、4等。然后我们将其缩小到四分之一,以便范围从0–4变为0– 1。
取模为5之后的颜色
即使渐变呈现规则性地循环,但最终的颜色在第一次偶然检查时看起来像是随机的,因为它与分形的几何结构不完全匹配。唯一真正明显的模式是中央列始终为黑色,因为它由每个级别的第一个实例组成。当序列与几何对齐时,这种现象也会在更深的层次上显现出来。
我们可以通过调整序列的长度来更改模式,例如将其增加到十。这增加了更多的颜色变化,并使黑色列出现的频率降低,但这会让它们更加显眼。
取模为10之后的颜色
2.3 韦尔数列
创建重复梯度的一种稍有不同的方法是使用Weyl数列。简单地说,这些序列的形式为0X模1、1X模1、2X模1、3X模1,依此类推。因此,我们只得到落在0-1范围内的分数值,不包括1。如果X是一个无理数,则该序列将在该范围内均匀分布。
我们并不真的需要完美的分配,只需要足够的种类即可。X会使用0-1范围内的随机值。例如,让我们使用0.381:
0.000, 0.381, 0.762, 0.143, 0.524, 0.905, 0.286, 0.667, 0.048, 0.429, 0.810, 0.191, 0.572, 0.953, 0.334, 0.715, 0.096, 0.477, 0.858, 0.239, 0.620, 0.001, 0.382, 0.763, 0.144, 0.525.
我们得到的大部分是三步但有时两步递增的梯度的重复,但都有些不同。模式在21步后重复,但会偏移0.001。其他的值将产生不同的图案,并具有不同的渐变,可以更长,更短和相反。
在着色器中,我们可以使用一个乘法创建此数列,并将结果馈送到frac函数。
基于0.381的数列颜色
2.4 随机参数和偏移
使用分数序列的结果看起来是可以接受的,但我们仍然得到一些黑色的列。可以通过在每个级别上添加不同的偏移量来消除这些偏移,甚至可以在每个级别上使用不同的序列。为此,请为两个序列号添加一个着色器属性向量,第一个为乘数,第二个为偏移量,然后在GetFractalColor中使用它们。需要在间隔值的小数部分之前添加偏移量,以便将偏移的偏移量应用于数列。
在Fractal中跟踪着色器属性的标识符。
然后为每个级别添加一个序列号数组,初始设置为等于我们当前的配置,即0.381和0。为此,我们使用Vector4类型,因为即使仅使用四分量矢量也可以发送到GPU,哪怕是只有2个components。
通过在属性块上调用SetVector,在Update中为每个级别的绘制循环中设置数列号。
最后,为了使数列在每个级别上具有任意性和不同性,我们将固定配置的序列号替换为随机值。我们将为此使用UnityEngine.Random,但是此类型与Unity.Mathematics.Random冲突,因此我们将显式使用适当的类型。
然后,要获得随机值,只需将两个常量替换为Random.value,这将产生一个介于0–1范围内的值。
带随机因素和偏移量数列的颜色
2.5 两个渐变
为了将随机数列与我们现有的渐变相结合,我们将引入第二个渐变并将两种颜色都发送到GPU。因此,将单色属性替换为A和B颜色的属性。
还要把A和B渐变替换单个可配置的渐变。
然后在Update的绘制循环中评估两个渐变并设置其颜色。
两个渐变属性
另外,将FractalGPU中的单色属性替换为两个。
并使用lerp在GetFractalColor中在它们之间进行插值,并将数列结果作为插值器。
最后,对于#else情况,只需返回A颜色。
使用两个渐变后的颜色
请注意,结果不是每个实例在两种颜色之间进行二选1,而是混合。
3 叶子
植物的一个共同特性是其末端特例化。比如树叶,花朵和果实。我们可以通过使最深层次不同于其他层次来将此功能添加到分形中。从现在开始,我们将考虑叶子级别,即使它可能不代表实际的叶子。
3.1 叶子颜色
为了使分形的叶子实例与众不同,我们将为它们赋予不同的颜色。尽管我们可以通过渐变简单地完成此操作,但是更容易地单独配置叶子颜色,将渐变专用于树干,树枝和嫩枝。因此,将两种叶子颜色的配置选项添加到Fractal。
叶子颜色属性
在Update中,确定绘制循环之前的叶子索引,该索引等于最后一个索引。
然后在循环内部,直接将配置的颜色用于叶子级别,并评估所有其他级别的渐变。同样,由于现在我们要提前一个步骤结束渐变,因此在计算插值器时必须从缓冲区长度中减去2而不是1。
具有明显叶子颜色的分形
注意,这种变化迫使我们再次增加最小分形深度。
3.2 叶子Mesh
现在,我们对最低层进行了不同的处理,我们还可以使用其他网格来绘制它。为此添加一个配置字段。这样就可以将立方体用于叶子,而将球体用于其他所有东西。
叶子的Mesh属性,设置为立方体
在Update中调用Graphics.DrawMeshInstancedProcedural时,请使用适当的网格。
叶子变为立方体
除了看起来更有趣之外,使用立方体叶子还可以显着提高性能,因为现在大多数实例都是立方体。我们最终得到的帧速率介于只绘制的球体和只绘制的立方体之间。
3.3 平滑度
除了不同的颜色,我们还可以使叶子具有不同的平滑度。实际上,我们可以根据第二个顺序来更改平滑度,就像我们更改颜色一样。要配置第二个数列,我们要做的就是在OnEnable中用随机值填充数列号向量的其他两个分量。
然后,我们将使用另两个已配置的A通道编号在GetFractalColor中分别插值RGB和A通道。
我们这么做是因为从现在开始,我们将使用颜色的A通道设置平滑度,这是可行的,因为我们不将其用于透明度。这意味着在我们的shader graph中,我们将使用Split节点从FractalColor中提取Alpha通道并将其链接到master的 smoothness上。然后从黑板上删除平滑属性。
导出平滑度
我们在表面着色器中执行相同的操作。
我们不应该重用一次调用GetFractalColor的结果吗?
是的,实际上我们已经在这样做了。着色器编译器可以识别并优化重复的工作。请注意,这总是发生在着色器中,但通常不会发生在常规C#代码中。
现在,可以从表面着色器中删除整个Properties块。
当我们使用颜色的Alpha通道控制平滑度时,我们现在需要调整颜色以考虑到这一点。例如,我将叶子的平滑度设置为50%和90%。请注意,即使通过相同的属性将它们配置在一起,也可以独立于颜色选择平滑度。我们只是利用迄今尚未使用的现有通道。
具有不同平滑度的黑色叶子
我们还需要对渐变执行此操作,默认情况下将其设置为100%alpha。我将它们设置为255中的80–90和140–160。我还调整了颜色以使分形更像树。
上色之后像一颗植物了
当分形深度设置为最大时,效果令人满意。
相同颜色配置,深度为8
4 下垂
尽管我们的分形看起来已经很“有机”,但这仅适用于其颜色。它的结构仍然是刚性的和完美的。这很容易从侧面看看出来,场景窗口处于正交模式,并且旋转在Update中暂时设置为零。
完美的刚体结构
有机结构并不会如此完美。除了生长过程中增加的不规则性外,植物最明显的是它们会受到重力的影响。一切都因自身重量而至少有所下垂。我们的分形不会遇到这种情况的影响,但是我们可以通过调整每个零件的旋转来近似该现象。
4.1 下垂旋转轴
我们可以通过旋转所有对象以使其下垂一点来模拟自然下垂。因此,我们必须围绕轴旋转每个实例,以使其局部轴轴看起来被拉低。然后,第一步是确定零件在世界空间中的向上轴。这是指向远离其父对象的轴。我们通过零件的初始世界旋转旋转向上矢量来找到它。必须在不考虑零件自身先前下垂的情况下进行此操作,否则它会积累起来,并且所有零件将下垂的非常厉害。因此,在调整零件的世界旋转之前,我们将基于零件的固定局部旋转及其父级的世界空间旋转在Execute的开头旋转。
如果一个零件不是垂直指向上,那么它自己的向上轴将不同于整个向上轴。通过绕另一个轴旋转,可以从世界的轴向上旋转到零件的轴向上。我们将这条轴命名为凹陷轴,它是通过交叉法,对两个轴进行叉乘得到的。
叉积的结果是一个垂直于两个自变量的向量。向量的长度取决于原始向量的相对方向和长度。因为我们正在使用单位长度向量,所以下垂轴的长度等于操作数之间的角度的正弦值。因此,要获得正确的单位长度轴,我们必需要将其调整为单位长度,为此我们可以使用归一化方法。
4.2 应用下垂
现在我们有了下垂轴,可以通过调用quaternion.AxisAngle来构造下垂旋转,该轴的角度和弧度为弧度。让我们创建一个45°的旋转,即四分之一π弧度。
要应用下垂,我们需要不再将零件的世界旋转直接建立在其父零件的基础上。相反,我们通过将下垂旋转应用于父级的世界旋转来引入新的基本旋转。
顶部已经消失
这产生了明显的差异,显然是不正确的。最极端的错误是分形的顶部几乎丢失了。发生这种情况的原因是,当零件垂直指向上方时,它与世界的上轴之间的角度为零。叉积的结果是长度为零的向量,对其归一化失败。我们通过检查下垂矢量的大小(其长度)是否大于零来解决此问题。如果是这样,我们才进行下垂处理,否则我们将不使用下垂,而直接使用父级的旋转。这具有物理意义,因为如果零件笔直指向上方,则处于平衡状态并且不会下垂。
向量的长度(也称为大小)可以通过Length方法找到。此后,如果需要,可以通过除以向量的大小将向量设为单位长度,这也是归一化需要做的。
顶部回归了,但是畸形的
分形仍然是畸形的,因为我们现在有效地应用了每个零件的方向两次。首先在下垂时,然后在特定方向上偏移时。我们通过始终沿零件的局部上轴偏移来解决此问题。
统一下垂45度
请注意,这意味着我们不再需要追踪每个零件的方向矢量,并且删除所有与之相关的代码。
4.3 调制下垂
下垂似乎有效,但在分形运动时观察它也很重要,因此请使其再次旋转。
修复下垂
它起作用了。无论零件的方向如何,它似乎都会被拉下。但是方向会突然改变。当下垂的方向改变时,会发生这种情况。因为我们使用固定的下垂角度,所以唯一的选择是沿正向或负向下垂,或者根本不下垂。这也意味着对于几乎指向下方的零件,下垂旋转最终会导致过头,而将其向上拉。
解决方案是让下垂量取决于世界向上轴和零件向上轴之间的角度。如果零件几乎垂直向上或向下指向,则几乎不会下垂,而如果零件完全指向侧面,则以90°角伸出,则下垂应最大。下垂量与角度之间的关系不必是线性的。实际上,使用角度的正弦会产生良好的效果。这就是叉积的大小,我们已经有了。因此,使用它来调节下垂旋转角度。
调制下垂
由于下垂是在世界空间中计算的,因此整个分形的方向都会对其造成影响。因此,通过稍微旋转分形游戏对象,我们也可以使其上垂。
分形绕Z轴旋转20°
4.4 最大下垂角度
分形现在下垂了,让我们可以配置最大下垂角度,通过暴露两个值以定义范围来再次增加下垂角度。我们使用度数来配置这些角度,因为比使用弧度更容易。
最大下垂角度
通过使用两个配置的角度作为参数调用Random.Range,将最大下垂角度添加到FractalPart并在CreatePart中对其进行初始化。可以通过弧度方法将结果转换为弧度。
A角必须小于B角吗?
尽管这是明智的做法,但这不是必需的。Random.Range方法仅使用随机值在其两个参数之间进行插值。
然后使用零件的最大下垂角,而不是在执行中使用恒定的45°。
可变下垂角度15–25,深度8
5 旋转
现在,我们已经对分形进行了很大的调整,使其看起来至少有些有机。我们要做的最后一个改进就是为其旋转行为增加多样性。
5.1 可变速度
就像我们对最大下垂角所做的那样,推进自旋速度范围的配置选项,以每秒度为单位。这些速度应为零或更大。
旋转速度
将自旋速度变量添加到FractalPart并在CreatePart中对其进行随机初始化。
接下来,删除UpdateFractalLevelJob中的均匀旋转角增量字段,将其替换为增量时间字段。然后在执行中应用零件自身的旋转速度。
调整Update后,它不再使用统一的旋转角度增量,而是将时间增量传递给作业系统。
旋转速度在0到90之间变化
5.2 反向旋转
我们可以做的另一件事是反转某些零件的旋转方向。这可以通过配置负的自旋速度来完成。但是,如果我们要同时混合正速度和负速度,则我们的两个配置值必须具有不同的符号。因此,范围会经过零,并且无法避免低速度。
解决方案是分别配置速度和方向。首先将速度重命名为速度,以表明它们没有方向。然后为反向旋转机会添加另一个配置选项,以概率表示,因此值在0–1范围内。
速度和反向旋转
我们可以通过检查随机值是否小于反向旋转机会来选择CreatePart中旋转的方向。如果是这样,我们将速度乘以-1,否则乘以1。
不同的旋转方向,速度始终为45°
请注意,现在分形的某些部分可能显得相对静止。因为当相反的自旋速度相互抵消时,会发生这种情况。
5.3 性能
在进行了所有调整之后,我们再次回顾性能。事实证明,更新时间增加了,深度6和7大约增加了一倍,而深度8则增加了30%。与上次测量相比,这对帧速率没有太大的负面影响。
本文翻译自 Jasper Flick的系列教程
原文地址:

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

楼主热帖
人人为我 我为人人 互相分享 互相学习 互相进步 一带一路
温馨提示:求助请到“Unity技术讨论”版块中发帖,便于集中解决!
您需要登录后才可以回帖 登录 | 立即注册

Unity游戏引擎开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2021-2-27 08:10 , Processed in 0.097439 second(s), 37 queries .