Blender仿真
Blender仿真
众所周知,blender是一款十分优秀的3D影视建模软件。但同时,由于其开源及优秀的拓展性,我们也可以在其中像Houdini一样写代码。
0. 准备工作
0x00 熟悉面板
注意
本文默认读者已经了解blender的基础操作逻辑,将不会在基础操作上多做赘述。

打开一个项目,按下a x删掉所有东西,并切换到Scripting一栏

可以看到,右边有一个脚本区,左边是3D视图和终端
我们按new
然后在下面输入代码
import bpy # 导入blender支持库
bpy.ops.mesh.primitive_cube_add() # 添加一个立方体运行后效果如下
提示
我们打代码调试时经常要使用print,但blender的终端是不会输出这些io的。因此,建议MacOS用户打开blender的可以执行文件:

这样,输出的调试信息就有啦
0x01 拓展软件
鉴于我们仿真时经常需要用到外部的库,例如pandas处理数据或者numpy做运算,我们也需要可以在blender版的内置python上可以这样操作。但在我的测试下我发现blender是不支持pip装库的:
因此,我的解决方案是anaconda在blender内新建一个环境
conda create --name=blender # 反正就是到blender内部py的目录
conda activate blender
conda install python=3.10 # 笔者使用的是Blender 4.3,只支持py3.10
conda install xxx # 其他库
sudo ln -s /opt/anaconda3/envs/blender /Applications/Blender.app/Contents/Resources/4.3 # 如果你是其他OS取决于你安装的位置和blender内库的结构这时候看运气了 运气好blender还能打开 运气不好只能通过可执行文件打开
0x02 Blender BPY API
开发者可以通过一个叫bpy的库来和文件内的objects和data进行交互
import bpy
bpy.ops.mesh.primitive_cube_add() # OPS: 操作符 Ex 添加一个立方体primitive
cube = bpy.context.active_object # Context: 界面上下文 Ex 获取当前选中的物体
cube.name = "My Cube" # 设置名称
frame = 1
bpy.context.scene.frame_set(frame) # 设置界面当前帧为1
cube.location.z = 0 # 设置位置
cube.keyframe_insert("location", frame=frame) # 插入关键帧
frame = 100
bpy.context.scene.frame_set(frame) # 设置界面当前帧为100
cube.location.z = 10 # 设置位置
cube.keyframe_insert("location", frame=frame) # 插入关键帧
1. 项目 - 重力场建模
1x00 数学建模
假设一个质点处于坐标系中央,则重力场向内指
而且我们知道重力场符合平方反比律:
提示
反平方定律 适用于三维空间中的现象,例如光、声音或引力等场的强度。这些场的强度会随着到场源距离的平方成反比递减。这种关系源于三维空间的几何特性,即场均匀地分布在球面上,而球面的面积随着 $$4\pi r^2$$ 增加。
其中,$$d$$ 表示到场源的距离。
因此
k是某个常数
我们把他按分量展开则
如果不在原点,则
如果有多个质点也不麻烦,总重力场就等于它们的重力场的线性组合
1x01 开始建模
本次我将使用OOP的思路
首先,定义一个class
from typing import Tuple
from math import sqrt
class Mass:
def __init__(self, k: float, pos: Tuple[float, float, float]):
"""
initialization method
"""
self.k = k
self.x = pos[0]
self.y = pos[1]
self.z = pos[2]
def __call__(self, pos: Tuple[float, float, float]):
posx = pos[0]
posy = pos[1]
posz = pos[2]
r = sqrt((self.x - posx)**2 + (self.y - posy)**2 + (self.z - posz)**2)
fieldx = -(self.k * (posx - self.x)) / (r**3)
fieldy = -(self.k * (posx - self.y)) / (r**3)
fieldz = -(self.k * (posx - self.z)) / (r**3)
return (fieldx, fieldy, fieldz)这个class可以存储一个质点的信息
我们再来一个class
from typing import List
class GravitationalField:
def __init__(self, masses: List[Mass]):
self.masses = masses
def __call__(self, pos):
x, y, z = 0, 0, 0
for mass in self.masses:
field = mass(pos)
x += field[0]
y += field[1]
z += field[2]
return (x, y, z)这个class类似一个高级的List[Mass], 可以直接调用他们的线性组合。当然列表也可以,但是用class是为了后期的可拓展性
1x02 场景交互
如果单单只是一个普通的建模,那其他软件也可以实现
Blender的优势在于可以使用各种Objects来可视化结果
这里,我准备将所有质点后缀改为M, 并添加属性k:



那怎么获取呢?
import bpy
masses = []
for obj in bpy.context.scene.objects:
if obj.name[-1] == 'M':
masses.append(obj)
fields = []
for mass in masses:
fields.append(Mass(mass['k'], mass.location))
sum = GravitationalField(fields)Sum 就是我们的主重力场
2. 可视化
2x00 数学建模
我现在构思的方案是在空间中放置箭头,用颜色来代表大小
那么
我们还需要一个将数字映射到颜色上的函数 为了方便起见,我就用线性插值了
提示
LERP (线性插值) 是一种根据比例在两个点之间计算值的方法,常用于图形学、动画以及数学中,以在两个值之间实现平滑过渡。
LERP 的公式为:
因此
我们也可以借python的one-liner语法写一个十分优雅的线性插值函数函数:
def color_lerp(color1, color2, k):
r, g, b = lerp(color1[0], color2[0], k), lerp(color1[1], color2[1], k), lerp(color1[2], color2[2], k)
return (r, g, b)2x01 实现
To assign colors to objects, we need to initiate new materials. Since this doc is not focused on the blender api itself, I'll use ChatGPT-generated helper functions to do the assignment.
要想给物体上色我们就需要给它赋予新的材质. 这不是今天的重点所以我就请我们亲爱的ChatGPT来写个Helper Function力
顺便一提ChatGPT的注释写的真的牛逼 😃
def assign_emission_shader(name, color, obj, strength=1.0):
"""
Assign an emission shader with the specified color and strength to the given object.
Parameters:
- name: The name of the material to be created.
- color: The RGB color for the emission shader (a tuple like (1.0, 0.0, 0.0) for red).
- obj: The Blender object to which the material will be assigned.
- strength: The strength of the emission (default is 1.0).
"""
# Check if the object already has the material assigned
existing_material = bpy.data.materials.get(name)
# If the material doesn't exist, create a new one
if not existing_material:
material = bpy.data.materials.new(name) # Create the material
material.use_nodes = True # Enable the use of nodes
nodes = material.node_tree.nodes
# Clear existing nodes
for node in nodes:
nodes.remove(node)
# Add an Emission shader node
emission_node = nodes.new(type='ShaderNodeEmission')
emission_node.inputs['Color'].default_value = (*color, 1.0) # Set the color with alpha = 1
emission_node.inputs['Strength'].default_value = strength # Set the emission strength
# Add a Material Output node
material_output_node = nodes.new(type='ShaderNodeOutputMaterial')
# Connect the emission node to the material output
material.node_tree.links.new(emission_node.outputs['Emission'], material_output_node.inputs['Surface'])
else:
material = existing_material
# Update the existing material's color and strength
nodes = material.node_tree.nodes
for node in nodes:
if isinstance(node, bpy.types.ShaderNodeEmission):
node.inputs['Color'].default_value = (*color, 1.0)
node.inputs['Strength'].default_value = strength
break
# Assign the material to the object
if obj.data.materials:
# If the object already has materials, replace the first one
obj.data.materials[0] = material
else:
# If the object has no materials, append the new one
obj.data.materials.append(material)然后我们就可以这样给每个物体刷颜色喽
s = 4 # Add to the start of code
for obj in masses:
k = obj['k']
c1 = (0.7, 0.3, 0.1) # Change to your preference
c2 = (0.1, 0.3, 0.7) # Change to your preference
color = color_lerp(c1, c2, k)
assign_emission_shader(obj.name, color, obj, s)删除材质
但你对你的代码进行调整之后, 你可能会发现材质没有任何区别. 这是因为blender的数据管理系统的问题. 这时你就可以在代码的一开始放上这么一段:
for material in bpy.data.materials:
if material.name[-1] == "M":
bpy.data.materials.remove(material)跑完代码之后可以看到材质都刷新了:
未完待续