[SD,fxmap]嵌套函数(fxmap详细介绍)
程序逻辑文章算法SDSubstanceDesigner
显示全部 10
2744 2
实名

通过了实名认证的内容创造者

发布于 2024-1-18 13:04:50

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

x
本帖最后由 大西几 于 2021-7-3 16:10 编辑

[SD,fxmap]嵌套函数(fxmap详细介绍)

文/大菜鸟

▌介绍:
使用软件 Substance Designer;
讲解内容 fxmap节点中嵌套函数的做法;
实用度 中等(或者很高?);
困难程度 相当难(针对一般SD用户来说);
学习价值 非常高;
使用插件 SDShortcutsEnhance
插件介绍与下载
戴巍:[SD Plugin] 快捷键创建节点重新排布插件(nuke UE4风格)​zhuanlan.zhihu.com


首先这是一篇关于Substance Designer软件中使用fxmap节点的教程,本来fxmap对于大多数人来说就已经很难了,而我今天要讲的内容在fxmap节点的使用上也算难的,所以是难上加难。
要能顺畅地跟下来这篇教程,需要有熟练的fxmap节点的使用能力,高中数学知识过关(向量),有基本的编程思维(嵌套函数和循环)。
本来很早就能写这篇教程的,但感觉国内用SD的大家好像还没搞懂fxmap是什么东西,所以也就一直没写,最近在群里讨论的时候,发现已经有一些朋友学的比较深入了,开始正面面对这个嵌套函数的问题了;时机开始成熟,终于到了有人能看懂这些东西的时候了。
这方面的资料非常少,几乎没有教程,只能扒一些官方做好的节点猜fxmap的运作逻辑。所以我这篇教程写出来应该非常有学习意义。
当然,学习要量力而行,很多基础知识的缺失不是一篇教程的篇幅能够容纳的。





▌实现目标: v2-35f65b476515ec0afbefef674af30d39_720w.jpg
有国外友人在SD里做过头发,这个就是用fxmap实现的。比较困难的点是,如果在做好每一根头发造型的基础上,又能很好地控制头发的根数。
或者是官方的scratch generator节点,也有类似的效果,每一根划痕都是由大量短小的直线连接而成(我故意把这个段数调低,图中可以看出一些直线结构),而整个图中又有大量的划痕线,这样的两层结构。




或者自然界中也有一些类似的结构:比如花菜,整体是一个大球,大球上又有很多中型的球形,中球上又有很多小球,一层层细分下去的结构。



或者叶脉结构也是分1级2级3级叶脉,这样的层级结构。



理论上用fxmap是比较容易做出一根由很多首尾相连的直线组成的线段的,如下图:



但是怎么把这一根扩展成很多根,这个问题可能会变得繁琐而困难。在群里跟朋友们讨论这个问题的时候,发现大家是用手工计算的方式,算出每一根大折线上的所有小折线的位移旋转等信息,再去生成;这样当然是可以做,但思考的逻辑非常不面向对象,会导致整个工程可控性比较低。



所以本文中将介绍一种降低耦合度的做法,就是所谓的嵌套函数。让1级结构的控制和2级结构的控制分开。这样逻辑清晰,非常方便制作以及修改。是我目前找到的这种问题的最优解。

假设生成大折线的实现函数是f(x),大折线的数量是M;生成小折线的函数是g(x),小折线的数量是N,那么我们可以写出如下伪代码:for (int i = 0 ; i < M ; i ++)
{        for(int j = 0 ; j< N ; i ++)        {               f(i,g(j));             }}

逻辑非常简单,后续的问题就是怎么定义,f(x) 与g(x)了。
本文限于篇幅,仅讲解最基本的实现技术与逻辑。像花菜和叶脉这些相对复杂的效果,可以在基础实现技术上修改实现函数来实现。大家学明白基础知识以后,可以自由发挥,做出自己想要的效果。




▌fxmap高级流程:
我们先来了解一些fxmap的高级功能,只有了解了这些,才能解决后续的制作问题。
因为这篇教程难度级别很高,所以默认假设前来学习的朋友已经掌握了fxmap的基本知识,这里就不过多介绍了。
首先是set 与get节点。
顾名思义,set是在fxmap内部创建一个自定义内部变量,可以是任意类型,由你的输入数据决定。set好了以后,再在别的地方用get节点获取这个变量,跟编程的逻辑完全一致。
下面我在iterate节点中设置了一个set节点,将一个float2类型的数据,赋予给offset变量,并且在quadrant节点的Pattern Offset 属性中直接调用这个offset变量作为输出。那么这一套操作,就可以让offset输入的float2直接控制图像的最终偏移。


set

get



这部分都是非常直觉而简单的,问题是set以后的输出问题。怎么把set的信息往后面的节点传递?
这里就是SD软件自己定义的一套做法了—— sequence节点。


我不清楚这个sequence的名字在这里有什么意义,但从功能上来说,很简单——它不起实际作用,仅仅作为输出打包。将所有挂在sequence节点上的set信息传递给后节点。
大家可以看到图中我传递了一个offset 一个size,这两个都很容易理解,而比较难理解的是最下面还有个int类型的1。
这个其实也是一个输出,但是这里输出的数据不是给后面节点用的,而是给当前节点的这个graph所在的属性用的。我这个graph是挂在Iterate节点的Iterations属性上的,也就是说这里的int类型的变量是就是Iterations的值,也就是循环次数。



我通过一些处理,让这个Iterations的赋值能看出变化效果:



最后要注意的是,这个输入的顺序是有影响的。作为当前属性的赋值参数,必须放在最下面的口子输入,而后续的set节点顺序则没什么影响。




以上就是我们需要知道的fxmap的基本高级功能,有了这些知识才能了解后续那些“连连看”是什么意思。




▌Main()函数的概念与应用:
你们看的一些教程里,会在具体某个属性里做计算逻辑。比如说offset相关的计算,放在Quadrant 的offset属性里去做。


这样做当然可以,但是一个复杂的fxmap,会对offset ,size , color等等诸多属性进行计算操作,并且甚至有两个属性里的参数相互影响的情况。这个时候,进进出出每个属性去调节相关计算逻辑,会非常繁琐麻烦。

所以这篇教程后面,我们会利用get set机制,将所有的计算操作,全部放在一个面板里进行,而最终在Quadrant节点调用计算结果的时候,不做任何的计算,仅仅get某个变量的最终值:




于是这个承载所有计算的节点属性,就变成了一个所谓的Main()函数。这些规则都是我们自己制定的流程,是方便后续制作的一种方案优化。



但这样做就有一个问题。现在的运算逻辑看起来是循环了11次,但画面里看起来只有一个方块。


为什么会这样呢?是因为,我们每个方块的偏移量是靠 $number * 0.082 来控制的。其中$number是系统变量,是SD给我们预先做好的固定变量,这个不是我们自己设置的。
它代表的含义是,上一个iterate节点的循环次数。而我们现在的这个设置,所有的计算已经发生在iterate节点中了,上面没有iterate节点了,所以这里的$number获得的值是0了。

那么这个时候我们想要做循环该怎么办呢?按照前面的逻辑,很容易猜到,需要再建立一个iterate节点放在当前iterate节点的前面来提供循环次数:




这里补一个课后思考问题:
现在我的第一个Iterate节点额循环次数是4,第二个Iterate节点内部设置的循环次数是11,最后画面中显示的是循环了4次。那么我们第二个Iterate节点中给的11次循环次数起什么作用,有什么意义? 请大家好好思考这个问题,可以在评论区讨论,我也会在评论区公布答案。想清楚之后再翻评论,看自己思考的对不对。
如果想对了,说明你对前面部分讲解的内容应该理解了不少。恭喜。



这一节,我们描述了一套在fxmap中做东西的更科学的新流程。对于我们后续做的相对比较复杂的计算来说,这套流程是非常有必要的。
准备知识都讲解完毕,后续开始具体制作。




▌计算第一根小直线的输出属性:

首先我们在fxmap中用小直线首尾相连,生成一根上面“实现目标”一节中,gif图演示的折线,也就是定义g(x)。
我们先研究第一根小直线的生成方式。我们先假设一个直线的生成点 startPos,默认情况下,直接生成出来的图案,中心点会在我们指定坐标处;如果我们想要一个小直线的一个端点生成在指定的startPos,就必须做偏移。这个偏移量是(curSize.x/2,0),再经过θ角度的旋转之后的向量。


我们按照之前的逻辑,最后需要输出的值是一个方块的curSize,curOffset,curRotation。cur是current的缩写。
其中curSize是我们直接填参数指定的,一般会指定(0.1,0.005)这样的值,让方块看起来是一根长条。
curRotation也是我们手动指定的值,范围是0-1,0不旋转,1旋转360度。并且这里我们会引入一个random来让每一次循环的旋转值带有随机。
curOffset则比较麻烦一点,是需要通过size 和rotation来计算的值。
对于一个向量(x,y)旋转θ角度,按照3blue1brown的线性代数课程(

【官方双语/合集】线性代数的本质 - 系列合集_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili&#8203;www.bilibili.com
)可以知道:
二维空间的旋转矩阵是

思考的方式是,原来的x轴单位向量(1,0)经过旋转以后,变成(cosθ,sinθ),这就是矩阵的第一列;原来的y轴单位向量(0,1)经过旋转以后,变成(-sinθ,cosθ),这就是矩阵的第二列。
对于任意坐标(x,y)经过θ角度的旋转以后有:
x′ = cosθ * x - sinθ * y;
y′ = sinθ * x + cosθ * y;
具体的推理过程请参考链接教程的讲解。


于是curOffset = startPos + (cosθ * curSize.x / 2, sinθ * curSize.x / 2) ;
对于第一根直线来说,我们可以让他产生在(0,0)点,于是startPos = (0,0);
以上,我们就完全确定了一根线段的curOffset , curRotation 和 curSize,这样就能完全决定第一根直线的形态。思路理清了以后,直接照抄公式“连连看”就行了(使用某个神秘插件连节点效率更高哦):




完成这一步之后,调整curRotation的值,可以看到我们的第一根直线是沿着一个端点在旋转的:




这一节我们了解了一根直线自身的参数计算,学习了在二维平面旋转向量的计算方法。
之后我们在这个基础上继续生成后续的直线,记得要首尾相连哦。



▌计算后续循环中小直线的输出:
因为要首尾相连,第二根直线的startPos 就是第一根直线的endPos。
然后所有的其余的计算,完全可以套上面的一节的公式。再看看这个图, 很容易得出endPos的计算公式:
endPos = startPos + (cosθ * curSize.x , sinθ * curSize.x ) ;



已经算出endPos的位置,那么现在的关键点在于怎么传递这个循环的endPos 到下一个循环的startPos。这部分内容正好体现在fxmap里做循环时set get参数的一种核心思想。

这里就要涉及到编程里经常用到的一个思路了——我们会使用一个叫做lastPos的变量,暂存上一次循环计算完的endPos数值,并且在这一次循环中使用lastPos的数值作为这次循环的startPos。
所以我们在原来的节点基础上增加了如下节点,核心点就是这个lastPos变量的get 与set。我们计算出一个endPos,并且把它set到lastPos变量中去。然后再用一个get节点,同样get这个lastPos,那么下次循环就可以拿到这个暂存的lastPos。



为了确保拿取lastPos的信息符合我们的预期,set lastPos的时候,要放在set curOffset之后进行,因为如果先set lastPos 再 set curOffset,那么curOffset拿到的其实是这一次循环的 lastPos 。而先set curOffset 的时候,get 的lastPos 是上一次循环计算的结果,计算完了curOffset以后,再set lastPos ,lastPos里的数据又更新为这一次循环的结果。
这个过程稍微有点抽象,可以看下图图解。第一步是get lastPos,第二步是计算endPos,第三步是把endPos的值set 到 lastPos。所以计算过程中所有get lastPos的步骤都要发生在set lastPos之前,也就是像图中连线的时候,要连在上方。


通过上面的图示,大家应该更容易理清这个循环的逻辑。然后可能也会发现一个问题,对于第一根小直线来说,第一次get的这个lastPos是什么???

因为第一次循环的时候,我们还没有进行set操作啊!
所以这里会有个很大的问题,我们必须给第一次循环的lastPos设定一个初始值,而这个初始值在这个Main()函数里。你或许会想,我们可以判断循环是否是第一次,如果是第一次,使用某一个具体值的方式来设置:


(注意,第一次循环的时候$number值为0)


但是结果却不符合我们的期待,第一次循环的初始位置确实被我们设置到我们指定的一个负方向的位置,但是后续循环的初始位置全部都在(0,0)点。



这多半是因为在SD中,我们set lastPos的时候,依赖于先get lastPos,而get的时候, lastPos变量根本就没有生成,导致后续set lastPos都做不成功。
所以我们还是得想办法在第一次循环的时候就先set 好lastPos的初始值。
那么我们自然想到,得跳出循环之外,在循环开始之前就要进行设置。那么我们就需要在Main()函数发生的这个Iterate节点之前就进行设置。


在第一个Iterate节点的Iterations属性中,我们创建一套新的graph来set lastPos,这里的(0,0)就是它的初始值:


而且不要忘了最下面要挂载一个int值,代表循环次数。
最后的结果。



如果要让每一根小直线的角度不同,记得在这里插入一个random节点,让每一次循环的rotation值都不同。


这一节,我们介绍了在fxmap中,利用set get的功能,暂存一个变量来很智能地完成计算。
这套思想可以很好地扩展到更加复杂的形态计算中,大家掌握了就可以自己发挥啦。
现在我们已经能用小直线生成一根曲折的连续线段,后面我们将尝试把这根曲折线段进行复制。



▌复制这些曲折的连续线段:
我们可以很直觉地想到,前面我们已经做好了生成一根的功能了,那么我们是不是在这些功能之前,再加一个iterate节点来决定到底要生成几根这样的折线呢?



我们按照直觉直接连一下试试,居然真的起作用了。此处应该有一种油然而生的兴奋感!



这就是程序化的制作思路,我们做好一些模块以后,再去复用这些模块,用很少量的参数,就可以做出整体的变化效果。每一部分都是独立解耦的,都可以单独控制自己这一部分的效果,逻辑清晰易于维护!整个Substance Designer软件就是按照这个思路设计的。
到这里我们已经从另外一个大家不怎么接触的角度,再一次领略到SD软件的设计精髓。我们前面设计的流程,优势在这一刻体现得淋漓精致!
平复一下激动的心情之后,我们开始着手解决还存留的一些简单问题。折线都生成在一小块集中区域,这是缺乏旋转随机和位移随机导致的。我们现在开始给每一根折线都加上独立的旋转随机和位移随机。


我们在Main()函数上一级里定义一个lineRotation决定每一根折线的旋转值,定义一个linePos决定每一根折线的初始生成位置。并且都给0-1的随机。


然后在原来每一根小直线的旋转值上,整体加上这个lineRotation。



这样,对于一根折线里的每一根小直线来说,lineRotation的数值都是一样的。对于不同的折线来说,这个lineRotation的值又都是不同的。
这是因为random节点的计算方式,认的是上一个iterate节点的循环。假设最上一层的iterate节点将向下做3次循环,第二层的iterate节点将向下做2次循环,我画了一个图示来表示这个random节点将产生的结果。如果有不理解的,可以看看这张图好好感受一下,也可以看看最后的视频部分,有更充沛的讲解。



那么现在每一根折线都有属于自己的整体旋转值。



再给每一根折线都整体加上一个linePos的偏移。原理跟上面也是一样的。



最后位移也随机了。



我们通过最上层的iterate节点的迭代次数,就可以控制到底要生成多少跟折线了。 这是唯一一个真的仅仅决定循环次数的节点,完全只使用它的原始功能。不像他下面的两个兄弟,已经变成工具人了,iterations属性里,被我们塞满了计算逻辑。






最后完成的嵌套函数节点图:
第二层控制每一根折线的属性。


第三层控制每一根折线里的每一根小直线的属性。



这一部分我们终于完成了嵌套函数的完整结构,最终双层迭代展现了它的魔力。
sbs文件下载地址,仅学习交流用(nestedFunctions_tuto):

Trello&#8203;trello.com



▌视频教程:
[SD,fxmap]嵌套函数_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili&#8203;www.bilibili.com
因为这篇教程涉及比较多的操作部分,也有一些文字比较难记录的信息,非常久违地录了个视频教程,欢迎配合文字部分享用。记得素质三连,让我们用爱发电。



▌最后:
因为是偏重讲解实现技术的教程,没有去做表现效果,看起来很简陋没什么吸引力,但这个技术真的好用,很多非常高级的SD效果,可能真的要依赖这一套,所以我说这篇教程的实用性是中等偏高呢,能否偏高就看大家拿着这个技术具体怎么用了。
后面有机会的话,可能会基于这里介绍的技术,做一些具体的东西(flag)。

一开始玩这个软件觉得真心牛逼,那也是因为以前我对图像这块写代码也不熟悉,有一套工具能够实现很多要靠写代码做出来的效果,我是很震撼的。这两年厉害的东西见多了,也就见怪不怪了,反而慢慢开始摸到很多SD这个软件的天花板。
但这不说明我要放弃这个软件,反而我工作里一直在大量使用它。也有一些TA的朋友觉得这个软件不能写shader是个短板,我也不否认,但我觉得要取长舍短,尽量用它优势的地方。
当然我还不是在这软件里面做出了raymarching呢,这种处处受限,但又能最终把东西做出来的感觉,有点像是带着镣铐跳舞。

其实这些东西,用代码写是挺简单的关系,比连连看要清晰明朗很多。估计会写代码的很多人也不需要这样的教程了。 之所以一套看下来感觉是有些繁琐,还是因为SD这个软件的大环境有关。它有许多自己定义的规则,比如sequence这个节点,你在别的软件里就见不到,还有iterate节点和random节点之间的关系,虽然说通了很简单清晰,但是对于刚接触这东西的人来说,像天书一样。所以为了理清他这个软件自己的规范和逻辑,花了不少篇幅。希望对开始挑战比较难的技术的朋友们能起到帮助,目前估计全世界你也只能在我这里学到这些东西了。
之所以写这么详尽也是因为最近在看ryan brucks的raymarching教程看吐了,希望自己的东西能够更容易让人看懂吧。
满篇都是对这个软件,对图形技术的热爱。
can you feel it


END


评分

参与人数 5元素币 +15 活跃度 +38 展开 理由
xunmixunmi + 2 + 1 留下了没有技术含量的口水
lanch + 6 + 5 秀儿,是你吗?
xiazi12121... + 7 + 1 口水
KL呆呆L + 15 【感谢】楼主分享的内容!很棒!
愚不是渔... + 16 【点赞】这很有大网气质!

查看全部评分

天若有情天亦老, 人间正道是沧桑。
使用道具 <
飞猪趴窝  发表于 2021-5-8 16:30:15  
2#
蒙圈
回复 收起回复
使用道具
“SPARK”  发表于 2021-8-24 21:04:22  
3#
是深层次的东西,要努力把基础攻克在回来学习了
回复 收起回复
使用道具
您需要登录后才可以回帖 登录 | 注册

本版积分规则

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