https://sinestesia.co/blog/tutorials/python-icospheres/
系列教程
1:生成2D 网格
4:圆角立方体
5:圆与圆柱
完整代码
import bpy
from math import sqrt
# -----------------------------------------------------------------------------
# 设置
scale = 1
subdiv = 5
name = 'Icosomething'
# -----------------------------------------------------------------------------
# Functions
middle_point_cache = {}
def vertex(x, y, z):
""" Return vertex coordinates fixed to the unit sphere """
length = sqrt(x**2 + y**2 + z**2)
return [(i * scale) / length for i in (x,y,z)]
def middle_point(point_1, point_2):
""" Find a middle point and project to the unit sphere """
# We check if we have already cut this edge first
# to avoid duplicated verts
smaller_index = min(point_1, point_2)
greater_index = max(point_1, point_2)
key = '{0}-{1}'.format(smaller_index, greater_index)
if key in middle_point_cache:
return middle_point_cache[key]
# If it's not in cache, then we can cut it
vert_1 = verts[point_1]
vert_2 = verts[point_2]
middle = [sum(i)/2 for i in zip(vert_1, vert_2)]
verts.append(vertex(*middle))
index = len(verts) - 1
middle_point_cache[key] = index
return index
# -----------------------------------------------------------------------------
# 创建基础二十面体
# 正二十面体,可以由3组黄金数生成,具体自己百度
# 黄金比例
PHI = (1 + sqrt(5)) / 2
verts = [
vertex(-1, PHI, 0),
vertex( 1, PHI, 0),
vertex(-1, -PHI, 0),
vertex( 1, -PHI, 0),
vertex(0, -1, PHI),
vertex(0, 1, PHI),
vertex(0, -1, -PHI),
vertex(0, 1, -PHI),
vertex( PHI, 0, -1),
vertex( PHI, 0, 1),
vertex(-PHI, 0, -1),
vertex(-PHI, 0, 1),
]
faces = [
# 5 faces around point 0
[0, 11, 5],
[0, 5, 1],
[0, 1, 7],
[0, 7, 10],
[0, 10, 11],
# Adjacent faces
[1, 5, 9],
[5, 11, 4],
[11, 10, 2],
[10, 7, 6],
[7, 1, 8],
# 5 faces around 3
[3, 9, 4],
[3, 4, 2],
[3, 2, 6],
[3, 6, 8],
[3, 8, 9],
# Adjacent faces
[4, 9, 5],
[2, 4, 11],
[6, 2, 10],
[8, 6, 7],
[9, 8, 1],
]
# -----------------------------------------------------------------------------
# Subdivisions
for i in range(subdiv):
faces_subdiv = []
for tri in faces:
v1 = middle_point(tri[0], tri[1])
v2 = middle_point(tri[1], tri[2])
v3 = middle_point(tri[2], tri[0])
faces_subdiv.append([tri[0], v1, v3])
faces_subdiv.append([tri[1], v2, v1])
faces_subdiv.append([tri[2], v3, v2])
faces_subdiv.append([v1, v2, v3])
faces = faces_subdiv
# -----------------------------------------------------------------------------
# 添加对象到场景
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(verts, [], faces)
obj = bpy.data.objects.new(name, mesh)
bpy.context.scene.collection.objects.link(obj)
# BL2.8后的物体选择
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
# -----------------------------------------------------------------------------
# Smoothing
#bpy.ops.object.shade_smooth()
for face in mesh.polygons:
face.use_smooth = True
具体步骤
什么是二十面体 icosahedron
是一种有20个面的多面体。二十面体有几种类型。为了制作球,我们只关注凸正二十面体(也是最著名的一种)。更多特性请戳维基百科。
为什么用二十面体?与UV球体相比,二十面体拥有更均匀的几何分布。由于几何体的密度较高,在两极附近变形UV球体往往会产生奇怪的结果,而二十面体则会产生一个更均匀和有机的结果。
最重要的是,二十面体是不对称的,这有助于有机变形。
本教程是基于Andreas Kahler的原始二十面体代码,适用于Python 3和Blender。
设置
让我们开始导入,然后转到通常设置。
import bpy
from math import sqrt
# -----------------------------------------------------------------------------
# 设置
scale = 1
subdiv = 5
name = 'Icosomething'
# -----------------------------------------------------------------------------
# 添加对象到场景
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(verts, [], faces)
obj = bpy.data.objects.new(name, mesh)
bpy.context.scene.collection.objects.link(obj)
# BL2.8后的物体选择
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
在设置中,subdiv
将控制细分网格的次数,scale
是缩放参数,与上一教程中的参数非常相似。subdiv
设置为零将创建一个二十面体(而不是 icosphere)。
请注意, subdiv
设置为 9 ,会有超过 500 万个面。具体取决于您的硬件(或者是否想让 Blender 崩溃)。
将球体放入 ico-sphere
简单地细分二十面体只会得到一个精致的二十面体。我们需要确保顶点以类似于球体的方式聚集在一起。
为了实现这一点,需要确保添加的顶点位于单位球体上。单位球体是一个半径为 1 的“假想”球体。
可以用一个简单的公式确定单位球面上每个点(顶点)的位置,然后将坐标固定到它上面。更多维基百科
为此,将有一个固定到单位球体的vertex()
函数(并且也进行缩放)。
def vertex(x, y, z):
""" 返回固定在单位球体上的顶点坐标 """
length = sqrt(x**2 + y**2 + z**2)
return [(i * scale) / length for i in (x,y,z)]
制作基础二十面体
像之前的立方体一样,最简单的方法是手动输入顶点和面。
将其顶点视为三个正交的黄金平面的角。因为遵循黄金比例,所以是“黄金平面”。这些平面的顶点位于坐标(0,±1,±φ),(±φ,0,±1)和(±1,±φ,0)。
字母φ(phi)代表黄金比例值,而±表示 "负或正"。
结果就是12个顶点,形成20个等边三角形,每个顶点有5个三角形相交。请看下图(下面的面列表中,点是用数字表示的)。
然而,如果分割所有的边,会遇到已经分割过的相同边。这将导致大量重复线段,建面时候会让人头痛。
为了防止这种情况,保留一个已分割过的边的列表(一个缓存),并在分割前检查它。这个缓存将是一个字典。键是顶点的索引,从小到大排序。这样一来,传递边的顶点,键都会保持不变。
middle_point_cache = {}
def middle_point(point_1, point_2):
""" 找到一个中间点并投射到单位球体上 """
# 首先检查我们是否已经切割了这条边,以避免重复的顶点
smaller_index = min(point_1, point_2)
greater_index = max(point_1, point_2)
key = '{0}-{1}'.format(smaller_index, greater_index)
if key in middle_point_cache:
return middle_point_cache[key]
# 如果它不在缓存中,可以把它剪掉
vert_1 = verts[point_1]
vert_2 = verts[point_2]
middle = [sum(i)/2 for i in zip(vert_1, vert_2)]
verts.append(vertex(*middle))
index = len(verts) - 1
middle_point_cache[key] = index
return index
中间顶点是通过两个顶点的坐标相加并除以2,最后将其添加到缓存中,并返回索引以形成面列表。
细分
有了mid_point()函数,就可以转到循环并进行细分。
在每个细分级别,我们创建一个新的空的面列表,并在最后用新的面替换原来的面列表。然后遍历每个面,找到它的三条边的中间点,存储索引并从中创建四个新的面
# Subdivisions
for i in range(subdiv):
faces_subdiv = []
for tri in faces:
v1 = middle_point(tri[0], tri[1])
v2 = middle_point(tri[1], tri[2])
v3 = middle_point(tri[2], tri[0])
faces_subdiv.append([tri[0], v1, v3])
faces_subdiv.append([tri[1], v2, v1])
faces_subdiv.append([tri[2], v3, v2])
faces_subdiv.append([v1, v2, v3])
faces = faces_subdiv
平滑
现在有一个非常接近球体的网格,但看起来仍然是面状的。是时候让它变得平滑了。
平滑阴影是面的属性。
因此,要将整个网格设置为平滑,需要给所有的面启用平滑阴影。你也可以使用bl自带的平滑操作项完成
bpy.ops.object.shade_smooth()
操作项的上下文正确可以使用这个。
在其他情况下,可能由于 "不正确的上下文 "而拒绝运行。
Blender的上下文是一种神变量,包含了关于应用程序当前状态的信息。包括诸如鼠标光标位置、活动区域等。
可以在调用一个操作项时覆盖上下文,但目前没有简单方法知道每个操作项上下文是什么。
幸运的是,可以通过循环设置每个面来完成这个 "更低级 "的工作。
for face in mesh.polygons:
face.use_smooth = True
在Blender脚本的世界里,"低级 "是指跳过操作项,直接进入对象的方法和属性。 from_pydata()也是一个"低级"工作的例子。
低级的好处是,它不依赖于上下文,通常更灵活,并节省通过操作符系统的开销。在这种情况下,你也可以决定只对某些面进行平滑处理。