https://sinestesia.co/blog/tutorials/python-rounded-cube/
系列教程
1:生成2D 网格
4:圆角立方体
5:圆与圆柱
与上次不同,本教程将减少数学方面的内容,而更侧重于 "Blender的东西"。
我们将研究添加和应用修改器,从文件中读取网格和管理复杂性。
全部代码
import bpy
import json
import os
from math import radians
from random import random, uniform
from mathutils import Matrix
# -----------------------------------------------------------------------------
# Functions
def object_from_data(data, name, scene, select=True):
""" 创建网格物体,并连接到场景 """
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(data['verts'], data['edges'], data['faces'])
obj = bpy.data.objects.new(name, mesh)
scene.collection.objects.link(obj)
# 选择该物体
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
# 检查网格中的无效几何
# https://docs.blender.org/api/current/bpy.types.Mesh.html#bpy.types.Mesh.validate
mesh.validate(verbose=True)
return obj
def transform(obj, position=None, scale=None, rotation=None):
""" 应用变化 matrices 到一个物体或网格 """
position_mat = 1 if not position else Matrix.Translation(position)
if scale:
scale_x = Matrix.Scale(scale[0], 4, (1, 0, 0))
scale_y = Matrix.Scale(scale[1], 4, (0, 1, 0))
scale_z = Matrix.Scale(scale[2], 4, (0, 0, 1))
scale_mat = scale_x @ scale_y @ scale_z
else:
scale_mat = 1
if rotation:
rotation_mat = Matrix.Rotation(radians(rotation[0]), 4, rotation[1])
else:
rotation_mat = 1
try:
obj.matrix_world @= position_mat @ rotation_mat @ scale_mat
return
except AttributeError:
# I used return/pass here to avoid nesting try/except blocks
pass
try:
obj.transform(position_mat @ rotation_mat @ scale_mat)
except AttributeError:
raise TypeError('First parameter must be an object or mesh')
def apply_modifiers(obj):
""" 在物体上应用修改器 """
bm = bmesh.new()
dg = bpy.context.evaluated_depsgraph_get()
bm.from_object(obj, dg)
bm.to_mesh(obj.data)
bm.free()
obj.modifiers.clear()
def set_smooth(obj):
""" 给网格物体应用平滑着色 """
for face in obj.data.polygons:
# https://docs.blender.org/api/current/bpy.types.MeshPolygon.html?#bpy.types.MeshPolygon.use_smooth
face.use_smooth = True
def get_filename(filepath):
""" 基于bl相对路径,返回绝对路径 """
base = os.path.dirname(bpy.context.blend_data.filepath)
return os.path.join(base, filepath)
# -----------------------------------------------------------------------------
# Using the functions together
def make_object(datafile, name):
""" 制作一个正方体 """
subdivisions = 0
roundness = 2.5
position = (uniform(-5, 5), uniform(-5, 5), uniform(-5, 5))
scale = (5 * random(), 5 * random(), 5 * random())
rotation = (20, 'X')
# 懒得保存在本地,所以直接加了
mesh_data = {"verts": [[1.0, 1.0, -1.0], [1.0, -1.0, -1.0], [-1.0, -1.0, -1.0], [-1.0, 1.0, -1.0], [1.0, 1.0, 1.0], [1.0, -1.0, 1.0],
[-1.0, -1.0, 1.0], [-1.0, 1.0, 1.0]], "edges": [], "faces": [[0, 1, 2, 3], [4, 7, 6, 5], [0, 4, 5, 1], [1, 5, 6, 2], [2, 6, 7, 3], [4, 0, 3, 7]]}
# 读取本地文件
# with open(datafile, 'r') as jsonfile:
# mesh_data = json.load(jsonfile)
scene = bpy.context.scene
obj = object_from_data(mesh_data, name, scene)
transform(obj, position, scale, rotation)
set_smooth(obj)
mod = obj.modifiers.new('Bevel', 'BEVEL')
mod.segments = 10
mod.width = (roundness / 10) / (sum(scale) / 3)
if subdivisions > 0:
mod = obj.modifiers.new('Subdivision', 'SUBSURF')
mod.levels = subdivisions
mod.render_levels = subdivisions
# apply_modifiers(obj)
return obj
# -----------------------------------------------------------------------------
#
try:
make_object(get_filename('cube.json'), 'Rounded Cube')
except FileNotFoundError as e:
print('[!] JSON file not found. {0}'.format(e))
except PermissionError as e:
print('[!] Could not open JSON file {0}'.format(e))
except KeyError as e:
print('[!] Mesh data error. {0}'.format(e))
except RuntimeError as e:
print('[!] from_pydata() failed. {0}'.format(e))
except TypeError as e:
print('[!] Passed the wrong type of object to transform. {0}'.format(e))
具体步骤
设置
我们将为立方体的网格数据创建一个JSON文件,然后根据它来构建网格。
这样就可以用任何网格数据替换,不需要改动代码。
接着转换网格,将其设置为平滑,添加修改器并(可选择)应用。代码有点多,但可以用函数包装起来,重复使用。
我们还将为网格变换添加随机性,这样每次运行脚本都会得到一个不同的立方体。最后将使用一个简单的公式来使斜面修改器在不同的比例下保持一致。
下面是导入的内容。我们需要OS包中的路径模块。
import bpy
import json
import os
from math import radians
from mathutils import Matrix
管理复杂性
在我们从文件中读取立方体数据之前,需要有一种方法将这些信息转换为网格和链接到场景中的对象。让我们开始把对象的制作抽象成它自己的函数。
首先要考虑数据如何被传递。可以接受一个JSON数据。
如何通过代码生成顶点呢?bpy.data.meshes.from_pydata() 需要顶点、边、面列表。
注意,如果你有面的列表,就不需要声明边列表,设置为空即可(否则会 KeyError)。
def object_from_data(data, name, scene, select=True):
""" 创建网格物体,并连接到场景 """
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(data['verts'], data['edges'], data['faces'])
obj = bpy.data.objects.new(name, mesh)
scene.collection.objects.link(obj)
# 选择该物体
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
# 检查网格中的无效几何
mesh.validate(verbose=True)
return obj
validate()函数检查网格是否存在无效的几何图形。
默认情况下,validate()只会在网格无效的情况下打印到终端。使用verbose参数可以打印更多信息,即使网格是有效的。
如果你要分享脚本,就把verbose参数关掉,避免打印多余信息。
接着来使用平滑代码,并打包进函数
def set_smooth(obj):
""" 给网格物体应用平滑着色 """
for face in obj.data.polygons:
# https://docs.blender.org/api/current/bpy.types.MeshPolygon.html?#bpy.types.MeshPolygon.use_smooth
face.use_smooth = True
从JSON文件中读取数据
我相信你一定在问 "JSON?为什么不是CSV?"
CSV用于表格数据和简单的列表,但我们要保存多个嵌套的列表。每个列表(verts或faces)都包含多个值的列表(三个或四个)。
JSON 在这方面的效果相当好,因为它与 Python 的数据结构一一对应。它也非常紧凑 (不像XML),而且方便编码/解码。
其他选择是YAML和OBJ,但需要第三方包来处理。当然格式随你选择。
我们先为立方体创建一个json文件。
{
"verts": [[1.0, 1.0, -1.0],
[1.0, -1.0, -1.0],
[-1.0, -1.0, -1.0],
[-1.0, 1.0, -1.0],
[1.0, 1.0, 1.0],
[1.0, -1.0, 1.0],
[-1.0, -1.0, 1.0],
[-1.0, 1.0, 1.0]
],
"edges": [],
"faces": [[0, 1, 2, 3],
[4, 7, 6, 5],
[0, 4, 5, 1],
[1, 5, 6, 2],
[2, 6, 7, 3],
[4, 0, 3, 7]
]
}
把verts和faces列表放在一个字典里,然后运行json.dumps()。然后我把结果粘贴到一个新文件中。
你也可以手动编写。将其作为cube.json保存在脚本同一文件夹中。
在读取文件之前,我们必须能够得到json文件的正确和绝对路径。由于它和blend文件保存在同一个地方,我们可以把Blender的路径工具和os.path混合起来,做成一个函数来制作这个路径。
def get_filename(filepath):
""" 基于bl相对路径,返回绝对路径 """
base = os.path.dirname(bpy.context.blend_data.filepath)
return os.path.join(base, filepath)
使用 os.path.join 可以确保它能跨平台工作。
现在我们可以读取文件,并将其传递给object_from_data()。
with open(get_filename('cube.json'), 'r') as jsonfile:
mesh_data = json.load(jsonfile)
scene = bpy.context.scene
obj = object_from_data(mesh_data, 'Cubert 2', scene)
还记得validate()中那个verbose参数吗?你会看到在终端打印出这样的信息。
BKE_mesh_validate_arrays: verts(8), edges(12), loops(24), polygons(6)
BKE_mesh_validate_arrays: finished
显然,你会看到一个熟悉的立方体坐在场景中间。
矩阵转换
将网格数据读入物体的过程已经完成。让我们把第二部分中的矩阵变换带回来,让事情变得更有趣。不是直接在物体上应用矩阵,而是通过一个函数来实现。
这个新函数负责从更简单的参数生成矩阵,无论传递的网格数据块还是物体,都可以应用它们。
参数非常简单:
- 要转换的对象或网格
- 位置,是一个三个值的元组
- 每个轴的缩放比例的一个元组
- 旋转是一个元组,其中第一个值是旋转度数,第二个值是一个字符串,代表要旋转的轴。必须是Matrix.Rotation()接受的一个字符串。
后面三个参数可选,如果省略,矩阵将被乘以1(与乘以一个相同的矩阵相同)。
如何检测第一个参数是什么?使用EAFP原则("寻求原谅比获得授权更容易")。对象有一个matrix_world属性,网格有一个transform()方法。可以尝试使用第一个方法,如果不行,再用第二个。
如果还失败,那么该参数不可转换,可以引发一个错误。
当以后调用这个函数时,这个异常可以被捕获。
def transform(obj, position=None, scale=None, rotation=None):
""" 应用变化 matrices 到一个物体或网格 """
position_mat = 1 if not position else Matrix.Translation(position)
if scale:
scale_x = Matrix.Scale(scale[0], 4, (1, 0, 0))
scale_y = Matrix.Scale(scale[1], 4, (0, 1, 0))
scale_z = Matrix.Scale(scale[2], 4, (0, 0, 1))
scale_mat = scale_x @ scale_y @ scale_z
else:
scale_mat = 1
if rotation:
rotation_mat = Matrix.Rotation(radians(rotation[0]), 4, rotation[1])
else:
rotation_mat = 1
try:
obj.matrix_world @= position_mat @ rotation_mat @ scale_mat
return
except AttributeError:
# I used return/pass here to avoid nesting try/except blocks
pass
try:
obj.transform(position_mat @ rotation_mat @ scale_mat)
except AttributeError:
raise TypeError('第一个参数必须是object or mesh')
放在一起,错误控制
现得到了我们的工具,来用它们做一些事情。
这次没有模块级的变量。而是把主要的代码放在一个函数里面。
这样做有两个理由:可以在不退出Blender的情况下捕捉错误并停止脚本,而且可以重复使用代码(比如把它放在一个循环中)。
def make_object(datafile, name):
""" 制作一个正方体 """
subdivisions = 0
roundness = 2.5
position = (uniform(-5, 5), uniform(-5, 5), uniform(-5, 5))
scale = (5 * random(), 5 * random(), 5 * random())
rotation = (20, 'X')
# 懒得保存在本地,所以直接加了
mesh_data = {"verts": [[1.0, 1.0, -1.0], [1.0, -1.0, -1.0], [-1.0, -1.0, -1.0], [-1.0, 1.0, -1.0], [1.0, 1.0, 1.0], [1.0, -1.0, 1.0],
[-1.0, -1.0, 1.0], [-1.0, 1.0, 1.0]], "edges": [], "faces": [[0, 1, 2, 3], [4, 7, 6, 5], [0, 4, 5, 1], [1, 5, 6, 2], [2, 6, 7, 3], [4, 0, 3, 7]]}
# 读取本地文件
# with open(datafile, 'r') as jsonfile:
# mesh_data = json.load(jsonfile)
scene = bpy.context.scene
obj = object_from_data(mesh_data, name, scene)
transform(obj, position, scale, rotation)
set_smooth(obj)
mod = obj.modifiers.new('Bevel', 'BEVEL')
mod.segments = 10
mod.width = (roundness / 10) / (sum(scale) / 3)
if subdivisions > 0:
mod = obj.modifiers.new('Subdivision', 'SUBSURF')
mod.levels = subdivisions
mod.render_levels = subdivisions
# apply_modifiers(obj)
return obj
圆角(roundness)和细分(subdivisions)变量控制斜面和细分修改器,将在下一节添加。
注意,我们在新函数中也采用了json文件的路径,这样就可以在不同的网格上运行整个程序。
现在可以通过在一个尝试块中调用这个函数来添加一些基本的错误处理。
try:
make_object(get_filename('cube.json'), 'Rounded Cube')
except FileNotFoundError as e:
print('[!] 未找到Json文件 {0}'.format(e))
except PermissionError as e:
print('[!] Json文件加载失败 {0}'.format(e))
except KeyError as e:
print('[!] 网格数据失败 {0}'.format(e))
except RuntimeError as e:
print('[!] from_pydata() 读取数据失败. {0}'.format(e))
except TypeError as e:
print('[!] 变换传递类型错误 {0}'.format(e))
你可以打印错误,记录它们,或者如果你是从操作项的execute()中调用这个,你也可以显示一个弹出窗口。
有趣的是,由于整个主代码都在make_object()中,可以在发现错误的时候停止脚本。事实上可以通过简单地从这个函数中返回,在任何一个任意的点上停止脚本。
如果把代码放在模块级而不是函数级,就将没有优雅的方式来停止脚本。虽然Python提供了sys.exit()和raise SystemExit()来停止执行,但这些也会杀死Blender。
关于错误处理,有足够多的内容可以写成一个单独的教程,但希望这能给你一些启发。
添加和应用修改器
这是一个关于圆角立方体的教程。那就把它们变圆吧! 可以通过手动改变顶点的坐标来做到这一点。Catlike coding在这方面有一个很好的教程(用于Unity)。
但这是Blender,我们有大量的修改器。来偷个懒,用斜面修改器来使立方体变圆。
bevel = obj.modifiers.new('Bevel', 'BEVEL')
bevel.segments = 10
bevel.width = roundness / 10
添加修改器就是这么简单。第一个参数是名字,第二个是修改器类型。
可以在API文档中找到修改器类型字符串的列表。 还可以用细分给立方体增加一些细化。
简单地确保subdivisions参数大于0来使其成为可选项。
if subdivisions > 0:
subdiv = obj.modifiers.new('Subdivision', 'SUBSURF')
subdiv.levels = subdivisions
subdiv.render_levels = subdivisions
我们可以应用修改器。没有自动的方法可以做到这一点(除了调用操作项)。只能将派生网格(由修改器产生的网格)和实际的网格数据替换成。然后再删除修改器。
这是你可能在很多地方都想做的事情,所以我们也为它做一个函数吧。
def apply_modifiers(obj):
""" 应用修改器到物体 """
bm = bmesh.new()
dg = bpy.context.evaluated_depsgraph_get()
bm.from_object(obj, dg)
bm.to_mesh(obj.data)
bm.free()
obj.modifiers.clear()
设置类型参数是两个字符串之一:"PREVIEW "和 "RENDER"。
这些参数控制修改器中的哪些设置被应用,视图或渲染。如果你只想应用视图级别,可以使用PREVIEW。
添加随机性
为了使代码更花哨一些,把一些部分随机化呢。
可以使用 Python 的随机模块。可以使用uniform()在一个范围内创建一个随机值,或者用随机值做一些数学运算(通常是乘法)。
random()在这方面很好,因为它返回0.0-1.0范围内的值。试着改变这几行,你每次运行脚本时都会得到一个不同的立方体
from random import random, uniform
# [...]
position = (uniform(-5,5), uniform(-5,5), uniform(-5,5))
scale = (5 * random(), 5 * random(), 5 * random())
现在我们正在改变物体的比例,如果能计算出斜面的大小以保持所有立方体之间的圆度一致,那不是很好吗?这就像除以比例的平均值一样简单。
mod.width = (roundness / 10) / (sum(scale) / 3)
有概率分母为0。如为了防止这种情况,可以把它放在try里
try:
mod.width = (roundness / 10) / (sum(scale) / 3)
except DivisionByZeroError:
mod.width = roundness / 10
按几下运行脚本按钮,立方体生成!