【零基础】SU组件自由落体
零基础 本系列将回顾和串联往期几篇文章中的部分内容,并通过具体的例子作尽可能详尽的操作介绍。其中的部分细节和特殊情况将不再重复描述,专注于 零基础 和 step-by-step 两个属性。
【本期导览】 | |
〇、原理总述 一、创建表面 二、组件的平面生成 三、下落的过程 四、后续调整和修饰 | “组件自由落体”即:规定的图元,在三维空间中向下移动到刚刚接触给定表面。可以用于在不规则表面(例如地形表面)上放置装饰组件。 |
〇、原理总述
有这样一个植树任务:确定一个地形表面,和几种树种组件模型,需要在地形表面上随机种植不同的树种。这个工作可以分成两步,第一步是平面上的随机生成若干树组件,第二步是根据地形表面给每一个树组件高度赋值。
第一步在平面上随机植树同样也可以是确切的平面设计,总之是先在平面图中完成植树。如果是随机创建,这里需要使用阵列复制和一个随机移动过程。其中前者使用 SU 自带的工具即可完成;后者不仅可以通过插件实现,也可以通过 ruby 控制台实现,下文的示例会采用第二种方式。
第二步则是将平面设计投影到具体的表面上,可以形象地解释为自由落体。这一步需要分别取得每一个组件的下界和对应空间上表面的高度,并作差值得到需要下落多少高度才能让两点相遇。
每一个图元都可以通过 .bounds 方法获得一个粗略的空间范围,其范围等于可容纳下图形的最小轴向立方体。如果是2D模型则返回包络圆柱体所对应的立方体。 |
经过立方体底面中心作铅垂线,寻找与表面的交点。两点之间距离即为要下落的距离,如果没有找到交点则不移动组件。
一、创建表面
地形创建有多种方式,可以使用 SketchUp 自带的地形工具创建,或是导入已有的模型,还可以自行绘制。无论哪一种表面,只要满足选择工具左键单击一次可以选取整个范围即可,此时选择表面后图元信息窗口如下图:
通常出于保护模型的考虑会将地形表面打组,但是无论组件还是群组都不是地形表面本身的结构,通过图元信息窗口可以查看当前选择的是否是表面本身。 |
关于表面,更详细的介绍可以查看 SU Ruby 教程中的相应篇目 [SU-R14]。另外,使用 ruby 代码创建地形可以参考此篇 [SU-2020-12]。
下图是在群组中选择表面的效果:
创建表面之后需要在控制台中用变量表示这个表面,这需要使用到选区功能。单击选择表面之后运行以下代码:
surf=Sketchup.active_model.selection.to_a;nil
以上代码将 surf 变量指向选中的表面,其中 selection 方法返回选区结构,而后 to_a 方法将选区中的图元保存到新的一个数组中。最后的 nil 则是避免控制台运行完此行代码后直接将数组输出到屏幕上的一个小技巧。
额外补充一下, to_a 方法来自 Enumerable 模块。由于选区 Selection 类包含有此模块,因此可以使用 to_a 方法。而单次点选一个表面时,选区中包含且只有构成此表面的平面图元,因此可以用平面的数组来表示一个表面,详细的介绍可以参考此篇教程 [SU-R14]。
二、组件的平面生成
选取一个树种组件阵列复制,并覆盖整个地形表面,并且空间位置在地形表面之上:
注意:此处组件是直接与表面在同一个群组内的,这很重要。如果用于下落的组件和表面不在一个层次内,会存在绝对坐标与相对坐标的问题,这里出于简化的目的,要求两者需在同一个群组内。
此时的树模型还是完全规整的排布方式,这时需要将这些组件随机移动。
首先打开 ruby 控制台,复制以下的代码:
module Apiglio
def self.random_2d_movement(ent,dist=1000.mm)
centre=ent.bounds.center
angle=rand(360).degrees
trans_h=Geom::Transformation.rotation([0,0,0],[0,0,1],angle)
rdist=Geom::Vector3d.new([dist*rand,0,0])
rdist.transform!(trans_h)
trans=Geom::Transformation.translation(rdist)
Sketchup.active_model.entities.transform_entities(trans,ent)
end
end
如果是早期版本的 SketchUp,控制台可能不支持多行粘贴代码,这时需要将这个代码保存到文本文档中(例如 “F:\Temp\RandMov.rb”),再使用以下方法加载这个脚本(请注意斜杠的不同):
load "F:/Temp/RandMov.rb"
以上代码定义了一个水平方向上随机移动的方法,将参数 ent 所代表的图元在水平方向上向任意某个方向移动 dist 的距离。例如以下示例代码将选中的唯一的一个(或第一个)图元随机水平移动1米以内的距离:
ent=Sketchup.active_model.selection[0]
Apiglio.random_2d_movement(ent,1000.mm)
其中第一行将 ent 指向选区中的第一个图元,而后第二行按照以上定义的方法随机移动这个图元,两行可以分别输入控制台依次执行。
如果要批量随机移动所有选择的图元,则需要使用选区的 .each 迭代器:
sels=Sketchup.active_model.selection
sels.each{|ent|Apiglio.random_2d_movement(ent,4500.mm)}
随机移动的距离要与阵列间距在同一个数量级(例如例子中使用4.5米,而阵列复制的间距为9米),否则随机效果不佳。以下是随机前后的对比:
由于随机的对象是树,因此这里只要考虑水平方向的移动,如果有垂直方向上的移动或者是旋转的需求,则需要将以上的方法进行修改,详见此篇 [SU-2021-01]。
三、下落的过程
对于单个需要下落的组件,首先需要定义一个函数用于返回具体一个点坐标到地形表面上的投影坐标。复制以下代码到 ruby 控制台:
def project_point_to_surface(point,surface)
plumb_line=[point,[0,0,1]]
pi=nil
if surface.find{|f|
pi=Geom.intersect_line_plane(plumb_line,f.plane)
if pi.nil? then false
elsif f.classify_point(pi)==Sketchup::Face::PointOutside then false
else true end
}.nil? then
return(nil)
else
return(pi)
end
end
以上代码定义的方法需要两个参数,第一个参数 point 为具体一个空间坐标,类型为 Geom:: Point3d(三维点坐标) 或 Array(数组)。第二个参数 surface 为一个表面,即前文描述中的“平面图元的数组”。
第2行的 plumb_line 为过点 point 所作的铅垂线。第3行创建一个名为 pi 的变量,并赋值为空值;此行如果删除,之后第5行所创建的变量的作用域将限定在 .find 方法的块参数之中(也就是说只在第4~9行之间有效)。第4~9行在表面中的每一个平面图元中寻找符合条件的结果,其中将铅垂线 plumb_line 和平面图元所在平面的交点记作 pi。如果 pi 存在并且在平面图元内,则代表 point 能够投影在 surface 上,那么整个方法的返回值即这个交点 pi;否则就继续寻找表面中的其他平面,如果所有平面都不符合条件则返回空值。整个4~13行是一个整体,它由 “if 条件 then 分支A else 分支B end” 嵌套了 find{}.nil? 的条件。
以下是一个简单的测试效果:
能够计算单个点坐标在表面上的投影以后,就可以将组件下界中心点坐标代入其中,计算出该点与投影点的距离。根据此距离进行组件移动即可实现下落效果。以下是代码:
def falling_onto_surface(grp,surf)
b=grp.bounds
bc=b.center-Geom::Vector3d.new([0,0,b.depth/2])
proj=project_point_to_surface(bc,surf)
return(nil) if proj.nil?
t=Geom::Transformation.new(proj-bc)
grp.parent.entities.transform_entities(t,grp)
end
以上代码中定义的方法需要两个参数,第一个参数 grp 表示需要下落的组件实例,第二个参数 surf 则为表面。第2行返回组件的边界对象 b,第3行得到边界底面中心点的坐标 bc。第4行计算投影点坐标 proj,第5行排除无法投影的情况,之后第6行根据投影点 proj 和底面中心 bc 的距离创建平移变换对象 t,第7行平移组件 grp。
使用一个简单的迭代器就能批量调用这个下落方法:
#以下代码原理上有效,但是不推荐使用
sels=Sketchup.active_model.selection.to_a;nil
sels.each{|g|falling_onto_surface(g,surf)}
但是并不推荐使用以上代码来进行下落操作。这是因为,当需要下落的组件非常多时,如果需要撤销下落操作,你将会发现,每一次撤销只会撤销一个组件的下落操作。这样是十分不友好的,因此应该使用以下方法:
def falling_selected_onto_surface(surface)
Sketchup.active_model.start_operation("Falling Onto Surface",true)
sels=Sketchup.active_model.selection.to_a
sels.each{|g|
if g.is_a?(Sketchup::ComponentInstance) or g.is_a?(Sketchup::Group) then
falling_onto_surface(g,surface)
else
Sketchup.active_model.abort_operation
return nil
end
}
Sketchup.active_model.commit_operation
end
以上代码中定义了一个仅需要一个参数的方法,参数 surface 代表表面,即平面图元的数组;此外还有一个隐含的参数,即选区中的图元;方法中会调用选区,依次操作每一个选中的图元。第3行即调用选区中的图元的代码。4~11行遍历每一个选区中的图元,如果是群组或者组件就调用 falling_onto_surface 方法移动该图元;如果不是符合要求的图元则表示选区参数有误,当即退出此方法,并且放弃之前的编辑。其中第2、8和12行完整地定义了一个可撤销的操作,以达到通过一次撤销命令还原到下落之前模型状态的目的:
选择需要下落的组件后在控制台中输入以下代码并运行:
falling_selected_onto_surface(surf)
就可以得到如下的效果(如果投影不在表面内的图元则不会下落):
四、后续的调整与修饰
由于下落元素是组件,还可以通过编辑组件,对所有树种进行统一的编辑,也可以进行相对位置的改变。这种微调能够在底部宽度不可忽略、且所在表面坡度较大时进行简易地修饰,以达到将基底埋藏在表面以下的效果。
另外还可以通过组件定义的替换,在保留组件实例坐标的前提下布置其他组件,不过这种方法成功与否取决于组件替换前后的坐标轴是否相适应。例如:可以将下落的树随机替换成不同种类的树种,以达到自然混合生长的效果。
首先需要整理好需要使用的树种模型:
之后将树种模型的定义存为一个列表:
defs=Sketchup.active_model.definitions
trees=[]
trees<<defs["Tree_1"]
trees<<defs["Tree_2"]
trees<<defs["Tree_3"]
trees<<defs["Tree_4"]
trees<<defs["Tree_5"]
tress[1]
以上代码执行完成后, trees 将是一个存有5个树种定义的数组,之后使用诸如 trees[1] 这样的表达就可以引用具体一个组件定义。对于一个组件实例 g,可以使用 g .definition = trees[1] 这样的表达将其改为相应的树种。
选择所有树组件后,执行以下代码:
sels=Sketchup.active_model.selection
sels.grep(Sketchup::ComponentInstance).each{|g|
g.definition=trees[rand(5)]
}
其中的 rand(5) 会返回0~4的的整数,相当于在 trees 数组中随机选择一个组件定义。执行后就可以得到以下效果:
如果下落以后不方便一次性选取所有树组件,也可以使用选区辅助代码来提高选择图元的效率,详见此篇 [SU-2021-05]。
(完)
本篇文章尽量从最浅层的内容开始解释,其中出现了表面图元、几何计算、随机变换、选区读取和组件替换等内容,可以参考以下几个篇目:
也可以参考更系统的教程:
本文编号:SU-2021-06