企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# **随机化石头的生成** * * * * * ## **零、需求分析** **H子**:**AOI**老师,最近有想法,想要制作一款独立游戏了,但是美术素材量并不少,特别是一些琐碎的资源,比如石头,树木之类的东西。 **AOI**:嗯。有考虑过到各种资源市场购买资源吗。 **H子**:有是有过,不过考虑到预算限制,以及资源风格,数量等等的问题,靠购买也并不能解决所有问题。 **AOI**:那么,有考虑过想要制作什么样的美术风格吗? **H子**:现在考虑到人力资源方面的压力,果然还是简单明快的风格更好吧。所以这次先考虑LowPoly风格了。 **AOI**:这个确实算是一种明智的选择。在独立团队,个人开发者等人力资源不足的情况下,选择更简单明快的风格,往往比需要庞大工作量的风格更有优势。 **AOI**:其次,尽可能实现程序化生成,也是可以提升开发效率的方法。 **H子**:程序化生成? **AOI**:对目标进行分析,并且找出其中的规律和逻辑,以此构建程序并自动化的产出素材。此后,就可以进一步的在自动产出的素材中筛选和修改,就可以更轻松的产出经过主观处理的素材。 这次就先用随处可见的石头举个例子吧。 想想看,Low Poly的石头有什么特点呢。 **H子**:我想想......随便拿粘土和硬卡,把几个面压平压出棱角的感觉? **AOI**:你说的好像也没什么错...... 首先,要生成随机的石头,可以先分析造型。一般的鹅卵石等的路边小石,多数因为自然风化等作用下,都是近似球形。先把受到外力破损的造型作为特例剔除。 也就是说,要生成随机的石头,需要的是以下几个基本步骤: 1. 在一定范围内的随机顶点 2. 把随机顶点最外层顶点转化成模型 生成一块石头所需要的,大体上来说就是以上的两个个基本步骤。 然后按照需求,逐步的完成石头的生成并且将流程自动化。 **H子**:那么!我们用什么方法来进行这个自动化生成的工作呢? **AOI**:那就是惯例的Blender吧。这次我们会使用一款叫Sverchok的可视化节点建模插件来进行这次的工作。 **H子**:好!我先去装好插件。 * * * * * ## **壹、生成位置的顶点【Random Vector MK2】节点的运用** **AOI**:先切换到Sverchok编辑界面吧。切换到节点编辑器,你会见到节点类型那里多了一个选项。 ![SV节点编辑器](https://box.kancloud.cn/585d071919f60363bb158a181b9992fc_114x33.png) 点击即可切换到Sverchok节点编辑界面。 **H子**:好了!我找到了。 **AOI**:首先进行第一步。 生成一定范围内的随机顶点,在Sverchok插件的内建节点库里面,有一个名为**Random Vector MK2**的节点可以使用。 按Crtl+空格键弹出的搜索节点窗口内输入节点名称即可搜索和创建该节点。 对了,创建节点前记得先创建节点树,虽然你不记得创建,在空白的节点编辑器界面直接创建节点,软件也会自动创建节点树。但是最好还是养成习惯比较好。 如下图所示: ![通过搜索框创建RandomVectorMK2节点](https://box.kancloud.cn/3b2053705c81450b5b494c0eb542ef65_301x280.png =301x280) 创建后,将会在画布上得到一个新的节点: ![RandomVectorMK2节点](https://box.kancloud.cn/e014c3d3be1e2c08d810a987820af0d9_179x140.png =179x140) 节点有三个入口: * Count:产生的随机Vector数量 * Seed:随机种子 * Scale:缩放 **H子**:也就是说,根据需要,调整Seed和Count值就可以获得需要的顶点了吗? **AOI**:根据实际情况输入Count的值,用于产生石头所需的顶点,顶点数量越多,越接近球形。因为随机顶点是在Scale值为半径的空间内产生随机顶点。而Scale值可以决定最后产生石头的大小的最大值,也就是产生顶点的区间的最大半径。 Seed可以随意设置,值的变化会产生不同的随机顶点,因此可以利用Seed进行造型的随机化,也是程序化石头批量生成的关键。 **H子**:那么?我们应该怎么把这些顶点变成最后的模型呢? **AOI**:先不用着急,我们可以先观察一下顶点的效果。 先通过搜索创建**Viewer Draw**节点: ![ViewerDraw搜索](https://box.kancloud.cn/28aad7e45a4b7e509ffe77832cbaea18_290x287.png =290x287) 创建之后,即可再画布见到该节点: ![ViewerDrawNode节点](https://box.kancloud.cn/a7516dee5c73ee8491a914f1e3f5e31a_140x206.png =140x206) 可以见到这个节点,有三个入口: * vertices:顶点 * egd_pol:边/面 * matrix:矩阵 **H子**:啊,我明白了,也就是说再对应的节点里面传递进去对应的数据,就可以在场景上画出对应的内容了吧。而且看入口的颜色就可以分辨出数据类型了。这么说的话,在**Random Vector MK2**节点的出口的颜色和**Viewer Draw**节点的vertices入口的颜色是一样的呢。 **AOI**:没错,所以可以直接吧这一对出入口链接在一起。那么,就连上去看看效果吧。 **H子**:连! ![1-1](https://box.kancloud.cn/76e1c5d5b1bcadb78ed8c80da5e71b14_467x604.png =467x604) 啊!场景上出现顶点了。 这么说,**Viewer Draw**节点上面的三个白色和蓝色的应该就是设置显示颜色了吧,分别对应点线面。 **AOI**:没错,从图标就确实的可以理解到其功能。 **H子**:那么,现在顶点有了,也可以显示出来了,那么,下一步就是该转换成模型了吧。 * * * * * ## **贰、将随机顶点变成模型吧【Convex Hull】节点的运用** **AOI**:好,我们先搜索找到Convex Hull节点吧。 **H子**:**AOI**老师!我搜出来两个结果了!一个叫**Convex Hull**一个叫**Convex Hull MK2**。他们有什么不同呢? ![ConvexHull搜索结果](https://box.kancloud.cn/e651fa40b09ff57a210b4db8e7ddaa46_279x264.png =279x264) **AOI**:Convex Hull节点顾名思义就是创建一个凸面包裹体的。 先创建两者的节点看看吧。 ![ConvexHullNode节点。](https://box.kancloud.cn/edd69f5e8aefc24d1dd69c311ed0a58c_311x196.png =311x196) 可以看到,两者光从外观上来看,差别还是挺大的,但是仔细观察接口的话,会发现两者的入口和出口都是完全一致的。 首先可以见到两者的入口都只有一个Vertices,从颜色即可看出,这个是接受顶点数据的传入。而输出则变成的两个,一个是Vertices,一个是Polygons。 由此可见,这两个节点的作用都是通过传入一组顶点数据,并且基于这一组顶点数据创建一个凸面物体。 而MK2和普通版的特点在于,MK2进一步的提供了包括2D的凸面形状的创建等的功能,而普通版的节点仅仅针对3D物体的创建。我们要创建的石头是3D物体,所以只需要使用普通版本即可。 把节点添加到节点树上试试看把。 **H子**:好!我试试看。 ![2-3](https://box.kancloud.cn/9db454cb034bbb32a44374d6c3e742b1_674x671.png) 我看到了! 出来了!好简单! 好,**AOI**老师你先不要说!我往下做做看。 点一下BAKE就能得到模型了对吧!对吧!对吧! **AOI**:冷静!冷静! **H子**:我点了! ![点击Bake并把模型拖出](https://box.kancloud.cn/24e08c5516646132d7196176dc30b4d2_465x261.png) 好!然后右键选择模型,拖出来。一模一样!成功了! **AOI**:对吧,要生成石头还是很简单的对不对。只要改变**Random Vector MK2**节点的**Seed**的值,就可以改变造型。当然现在的造型还很粗糙,想要进一步的进行更多的优化,自然要添加更多的节点对产生的数据进行控制。 * * * * * ## **叁、批量产生石头吧 【Frame Info】节点的运用** **H子**:那么下一步就是开始批量化的产生石头了吧。首先果然是要不断的变化**Seed**值呢,但是有什么方法可以不断的改变呢。手动点果然是不现实的呢,最讨厌这种重复的工作了。 **AOI**:仔细想想,在Blender或者一般的3D软件里面有什么值,是会不断的变化的呢?或者说,什么值是会自动递增的呢? **H子**:自动递增的值嘛?我想想看......啊!是时间!时间轴的值! **AOI**:对了,严格来说,就是帧数。当你开始播放的时候,这个是一个会不断递增的值。而在Sverchok的节点里面,有一个名为**Frame Info**的节点可以获得当前帧,并且作为值输出。 ![Frame Info搜索结果](https://box.kancloud.cn/1a18a80ec2e0eefce3a6a1a99f91fbe4_293x119.png) 搜索Frame Info就可以得到一个唯一结果。创建节点即可。 创建一个**Stethoscope MK2**节点,并且分别吧**Frame Info**节点的每个出口输出到**Stethoscope MK2**节点的Date入口看看有什么效果吧。 **H子**:好,我试试看! ![current frame](https://box.kancloud.cn/f9fa031eb258d05fe8c8f352d68b2224_400x218.png) ![Start frame](https://box.kancloud.cn/48607ac3af8ad2ee1cb97681a5807515_401x202.png) ![End frame](https://box.kancloud.cn/67c7fba20676b4d957f8f9d99a7338f1_392x205.png) ![Evaluate](https://box.kancloud.cn/8033080dc7b2820214c10ce05c5c3dea_421x213.png) 啊,我懂了,就是当前帧,起始和结束帧,以及当前帧位于起点到终点的百分比吧。 **AOI**:对的,就是这样。所以实际上会变化的数据只有两个,也就是CurrentFrame和Evaluate。 因此我们可以选择吧**CurrentFrame**出口直接连到**Random Vector MK2**节点的**Seed**入口。即可每一帧都改变产生的随机值了。当然,根据需要,可以使用**Math**节点对**CurrentFrame**输出的数据进行处理,然后再使用。 **H子**:好的!我连好了!但是总不能每次创建模型都要手动创建吧。 **AOI**:当然不是的,接下来,就给你说说看怎么进行自动化的批量生产吧。 * * * * * ## **肆、自动化产生石头吧!AnimationNode和脚本的运用**。 **AOI**:已经装好AnimationNode插件了吧,激活AnimationNode插件之后,就可以在节点编辑器窗口里面切换到AnimationNode界面了。 就是如图所示的小图标,点击切换过去就好了。 ![AnimationNode](https://box.kancloud.cn/7d6fc015824b6dba5a7b7e895d2fff21_123x44.png) 同样是先创建一个节点树吧。 **H子**:老师我做好了!但是,为什么要在这里使用另外一个插件呢。 **AOI**:在这里使用AnimationNode的主要原因,自然就是为了使用他的脚本节点了。使用脚本节点,可以根据需要执行脚本。 **H子**:啊,我懂了,也就是说,只要把BAKE按钮执行的命令,在AnimationNode的脚本节点里面运行就可以了对吧。 **AOI**:没错,你理解得很快呢。那么我们就开始操作吧。 首先,创建一个脚本节点。 对了,AnimationNode的搜索快捷键是**Ctrl+A**,要好好记住。 ![Script](https://box.kancloud.cn/887deca35c334b8d1eaff79a127290fc_332x292.png) 创建好脚本节点之后如图所示: ![Script NodeBase](https://box.kancloud.cn/ded8b49febc89867ef0022696da99302_242x185.png) 可以在这里选择或者新建一份脚本 ![ScriptSelection](https://box.kancloud.cn/e299e6fa5fea937ab66417de4c7616ea_177x32.png) 可以在此处修改脚本节点的名称,名称非常重要,因为之后对脚本节点的调用也是以名字为依据。 ![Name](https://box.kancloud.cn/89a8e86f93b9dce5a152fc2386f8aa5c_182x29.png) 在这里,我们就吧他改成AutoBake好了 然后,点击NewInput按钮即可创建不同的数据类型输入接口 ![New Input](https://box.kancloud.cn/1c5f49cdc70176d4b36e9f51dd39e5ca_206x40.png) Script节点的运用,我们就在后面一步一步的细说吧。 为了利用脚本,首先必须有一份脚本。可以通过脚本选择框旁边的+按钮创建,也可以在文本编辑器窗口创建,总之方法很多,无需拘泥。 在这里,就直接点击旁边的+按钮创建吧。 **H子**:我创建好了,程序自动的就根据我的节点名称创建了同名的脚本了阿 ![](https://box.kancloud.cn/e4086d9a559a485fe9f23c20cf5759a3_236x188.png) **AOI**:然后在界面上划分出文本编辑器的窗口,并且选择刚刚创建好的脚本。 **H子**:好的。 ![](https://box.kancloud.cn/9d53c56fbaf73a1240af5db88583ca21_799x516.png) 是一份空白的脚本呢。然后我就可以往里面编写代码了吗。 **AOI**:对的,就是这样。 接下来,就去获得BAKE模型的时候使用的命令吧。 切换回到Sverchok界面,并且点击一下BAKE按钮,拉出信息栏,即可见到BAKE操作对应的命令了。 ![bake Command](https://box.kancloud.cn/ff2affcffd6ea361a0760cd4becf464d_595x436.png) 右键选择该行命令,同时按Ctrl+C组合键复制命令。 然后再把命令粘贴到脚本里面。基本的操作就大功告成了。 ![](https://box.kancloud.cn/3b16216bb23605e0d8b39d2c7985587b_483x156.png) **H子**:那么老师,我有个问题。Script脚本,可以允许没有输入和输出接口,如果这样的话,不就是只要创建了脚本节点,并摆在画布上,不管有没有其他节点流程链接进来,都会每一帧都会执行一次了吗。 **AOI**:你说的很对,所以我们必须进行一些操作,确保不会自动的就执行代码,并且产生一大堆无用而重复的数据。 首先,切换到AnimationNode节点模式,按T打开工具栏,并切换到AnimationNodes工具栏 ![](https://box.kancloud.cn/cc8430ae2367b0d010618f712938d770_198x354.png) 在Auto Execution面板下面,去掉Always的勾,并勾上Frame Chaneged。这样的话动画节点树就不会总是执行,而只会在当帧有改变的时候才执行一次。 **H子**:等等老师,你这话的意思不就是在说,原本的动画节点树,是每一帧都在运行的吗? **AOI**:你说的并不完全对。 你看到AnimationNode节点编辑器左上角有个毫秒数的数字吗,那个就是执行完一次动画节点树消耗的毫秒数了。之所以一开始是0ms,是因为并没有任何节点在运行,所以也并没有任何时间的消耗。但是当你添加任意一个节点进入到画布的时候,时间就会有变动,也就是说,脚本被执行了。 也由此可见,实际上在AnimationNode中,每个节点的执行本质上是独立的,他们都会被主动调用,而通过出入口起来,只是为了让节点流的数据得以按照一个从左到右的方向传递和计算。 所以,关闭了Auto Execution的Always的意义,就在于让脚本不要时刻不停的自动执行下去。 因此,动画节点树并非每帧执行一次,而是不断的执行,与帧无关,只要全部执行完一次,就会自动开始第二次。 **H子**:那么为什么明明添加了Script节点,但是却没有执行呢。 **AOI**:这个问题问得好。其原因在于,因为Script节点,其实分成了两部分。一部分是用于定义,而一部分则是用于执行。创建Script节点的时候,实际上是创建了用于定义部分。只有把用于执行部分的节点创建到画布上的时候,才会执行。 **H子**:那么,我们该怎么添加执行部分的节点呢。 **AOI**:点击菜单栏的Subprograms按钮,再弹出的菜单栏即可见到刚刚创建和和命名为AutoBake的选项了。点击该选项,即可在画布上创建一个执行Auto Bake脚本 ![](https://box.kancloud.cn/dfb86f9f777b9f5068a53fd81ef8225a_478x338.png) ![](https://box.kancloud.cn/2c1364b3928db4d063f52a82888f322e_172x96.png) 可以看到,我们现在创建了一个既没有入口,也没有出口的名为AutoBake的**Invoke Subprogram**节点,点击中间的AutoBake按钮,即可在弹出的菜单选择并切换成其他的Script节点定义。 那么**H子**,我问你一个问题: 假设,我想让脚本每隔3帧执行一次,有什么办法可以做到呢? **H子**:我想想看......首先是要传递一个帧数据进来,然后在脚本里面添加判断,如果当前帧除以3并且余数为0的时候,就执行一次脚本......对吧。 **AOI**:没错,基本的思路就是这样子...... **H子**:欸?!奇怪了老师,为什么没有FrameInfo节点呢。 **AOI**:因为在AnimationNode里面传递帧的节点更简单,只传递当前帧,而且叫做**Time Info** ![](https://box.kancloud.cn/69d32b5f23978747b38a076c922ff918_141x73.png) 好吧,然后接下来为AutoBake的Script节点增加一个float型的数据入口吧。这样就可以把时间数据传递到Script节点内部,并且以变量的形式被代码调用。 如图所示,点击Script 的 New Input按钮,在弹出的搜索框内输入Float,即可搜到Float和Float List两个结果,Float List就是Float数据类型的列表,在此处,我们只需要使用一个数据即可,所以无需使用数组类型。 ![](https://box.kancloud.cn/c324e22489d291cab79616a10fc62b8a_441x195.png) 点击选择Float即可完成一个Float入口的创建。 ![](https://box.kancloud.cn/71457fb0bcd85145699b9c44ff79c962_199x281.png) 完成Float型入口的创建之后,默认的入口命名是Float,此处将其改成TimeInfo,入口的名称同时也是在脚本里面进行调用的变量名。 **H子**:老师我做好了,得到了变量之后,就是说我可以吧**Time Info**节点的输出值连到这个入口吧。然后只要进一步在代码里面设置判断条件就可以了吧? **AOI**:对的,那么,你试试看把代码完成看看。 **H子**:我已经写好了,老师看看这个对不对。 ~~~ if(TimeInfo > 0 and TimeInfo % 3 == 0): bpy.ops.node.sverchok_mesh_baker_mk2(idname="Viewer Draw", idtree="NodeTree") ~~~ **AOI**:嗯,不错,连帧值为0的时候也考虑到了,考虑得挺全面得。 这样直接执行就可以每隔3帧Bake一次模型了。只要打开时间轴,就可以得到大量得随机产生的石头了。 **H子**:我运行了一下,看起来是这样子的。 ![](https://box.kancloud.cn/9cfff3690221b06d1b866a73516f21f3_360x289.png) 确实达成目的了,每3帧执行一次,并且得到了模型。但是都叠在了一起了。 那不是只能一个个手动拖开了吗? **AOI**:那就交给你自己思考了。实际上在Sverchok的节点里面,有着很多可以操作各种数据类型的节点。可以考虑运用这些节点,对随机产生的数据进行操作。并且得到矫正过的数据。最后再转化成模型。 **H子**:好的!我想想去! * * * * * ## **终、**H子**的努力成果** **H子**最后在边查文档边记录操作脚本等一系列的努力下,把程序化石头生成器最终版本完成到了如下的程度: * 把Z轴高度小于0的顶点高度都设置成0,确保的石头底部扁平 * 烘焙出的模型自动进行对象和数据重命名 * 自动从色表上选取随机颜色,并添加到石头的顶点颜色上。 * 把烘焙的模型随机的分布在场景上,以避免模型的堆叠。 最终版本节点图: Sverchok节点图。节点树名称为【NodeTree】: ![SV节点图](https://box.kancloud.cn/b4b2d9729921e0a9dcb41540da2cb634_1752x471.png) Clamp节点图: ![Clamp节点图](https://box.kancloud.cn/29d386e6ba34ee05179b7fdb9de59005_698x199.png) AnimationNode节点图。节点树名称为【BakeStone】: ![AN节点图](https://box.kancloud.cn/d752b21f4b4d3e556bf97f540a6718f1_1320x690.png) AutoBake脚本: ~~~ FinObject = None if(TimeInput > 0 and TimeInput % 10 == 0): #执行Bake命令 bpy.ops.node.sverchok_mesh_baker_mk2(idname="Viewer Draw", idtree="NodeTree") #取消全选 bpy.ops.object.select_all(action='DESELECT') #obj获取对象名称为"Sv_0"的模型。因为Bake命令得到的模型默认为Sv_0 obj = bpy.data.objects["Sv_0"] #将模型设置为选中状态 obj.select = True #按照创建时的帧数为模型重命名,重命名包括对象名和多边形名 name = "Rock" + str(TimeInput) obj.name = name obj.data.name = name #将模型对象返回给输出节点,以便用于后续的操作 FinObject = obj ~~~ 最终完成的效果如图: ![随机石头](https://box.kancloud.cn/b4f72f16ed4a58df35d22bd62174ef61_384x235.gif) ![渲染图](https://box.kancloud.cn/1c34557c74afcfec6c36d5ccdb21d390_461x284.png) * * * * * *附注:因为插件本身并没有中文支持,如果使用中文Blender界面,插件的文本有部分会因为Blender自身的多语言支持特性而被翻译成中文,但是会使得插件本身的搜索功能变差,甚至无法通过搜索找到想要的节点。所以建议使用插件的时候尽量使用英文界面。*