from functools import reduce
import bpy
import bmesh
from bpy.types import Modifier
from mathutils import *
[docs]
class BlenderObject:
def __init__(self, obj: bpy.types.Object):
self._obj = obj
@property
def obj(self):
return self._obj
@property
def name(self):
return self.obj.name
@name.setter
def name(self, name):
self.obj.name = name
@property
def loc(self):
return self.obj.location
@loc.setter
def loc(self, loc):
self.obj.location = loc
[docs]
def hide(self):
self.obj.hide_set(True)
[docs]
class Axis:
def __init__(self, axis, start, end, world_matrix):
"""
:param axis: Vector of axis
:param start: Vector of start endpoint in object space
:param end: Vector of end endpoint in object space
:param world_matrix: 4x4 matrix of object to world transformation
:param local_coords: if coordinates given are in local space
"""
self._axis = axis
self._start = start
self._end = end
self._matrix_world = world_matrix
[docs]
def axis(self, local_coords=False):
if local_coords:
axis = self._axis.copy()
axis.rotate(self._matrix_world.to_quaternion().inverted())
return axis
return self._axis
[docs]
def endpoints(self, local_coords=False):
mat = self._matrix_world.inverted() if local_coords else Matrix.Identity(4)
return [mat @ self._start, mat @ self._end]
[docs]
def length(self, local_coords=False):
start, end = self.endpoints(local_coords)
return (end - start).length
# ----- BLENDER FUNCTIONS -----
[docs]
def create_mesh(
name,
loc,
mesh="icosphere",
size=1.5,
rotation=(0, 0, 0),
scale=(1, 1, 1),
subdivisions=2,
**kwargs,
):
bm = bmesh.new()
if isinstance(rotation, tuple):
rotation = Euler(rotation)
elif isinstance(rotation, Quaternion):
rotation = rotation.to_euler()
match mesh:
case "icosphere":
bmesh.ops.create_icosphere(
bm, subdivisions=subdivisions, radius=size, **kwargs
)
# bmesh.ops.beautify_fill(bm, faces=bm.faces, edges=bm.edges)
bmesh.ops.triangulate(bm, faces=bm.faces[:])
case "plane":
bmesh.ops.create_grid(
bm, x_segments=1, y_segments=1, size=size / 2, **kwargs
)
case "monkey":
bmesh.ops.create_monkey(bm, **kwargs)
case "cube":
bmesh.ops.create_cube(bm, size=size, **kwargs)
# bmesh.ops.beautify_fill(bm, faces=bm.faces, edges=bm.edges)
# bmesh.ops.triangulate(bm, faces=bm.faces[:])
case _:
raise ValueError(
"""mesh must be one of "icosphere", "plane", "monkey" or "cube"."""
)
me = bpy.data.meshes.new(f"{name}_mesh")
bm.to_mesh(me)
bm.free()
obj = bpy.data.objects.new(name, me)
obj.location = loc
obj.rotation_euler = rotation
obj.scale = scale
return obj
[docs]
def create_material(name, color):
# Create a new material
mat = bpy.data.materials.new(name=name)
r, g, b = color
mat.diffuse_color = (r, g, b, 1.0)
# Set material properties to create a matte finish
mat.metallic = 1.0 # No metallic reflection
mat.roughness = 1.0 # Maximum roughness for a matte finish
mat.specular_intensity = 1.0 # No specular reflection
mat.use_nodes = False # Ensure nodes are not used
return mat
# def create_material(name, color):
# mat = bpy.data.materials.new(name=name)
# r, g, b = color
# mat.diffuse_color = (r, g, b, 0.8) # viewport color
# mat.use_nodes = True
# mat.blend_method = "BLEND"
# # get the material nodes
# nodes = mat.node_tree.nodes
# nodes.clear()
# # create principled node for main color
# node_main = nodes.new(type="ShaderNodeBsdfPrincipled")
# node_main.location = -200, 100
# node_main.inputs["Base Color"].default_value = (r, g, b, 1)
# node_main.inputs["Metallic"].default_value = 1.0
# node_main.inputs["Roughness"].default_value = 1.0
# node_main.inputs["IOR"].default_value = 1.450
# # specular
# node_main.inputs["Anisotropic"].default_value = 0.041
# node_main.inputs["Anisotropic Rotation"].default_value = 0.048
# node_main.inputs["Alpha"].default_value = 0.414
# # create noise texture source
# node_noise = nodes.new(type="ShaderNodeTexNoise")
# node_noise.inputs["Scale"].default_value = 0.600
# node_noise.inputs["Detail"].default_value = 15.0
# node_noise.inputs["Roughness"].default_value = 1.0
# node_noise.inputs["Distortion"].default_value = 3.0
# # create HSV
# node_HSV = nodes.new(type="ShaderNodeHueSaturation")
# node_HSV.inputs["Hue"].default_value = 0.800
# node_HSV.inputs["Saturation"].default_value = 2.00
# node_HSV.inputs["Value"].default_value = 2.00
# node_HSV.inputs["Fac"].default_value = 1.00
# # create second principled node for random color variation
# node_random = nodes.new(type="ShaderNodeBsdfPrincipled")
# node_random.location = -200, -100
# node_random.inputs["Base Color"].default_value = (r, g, b, 1)
# node_random.inputs["Metallic"].default_value = 1.0
# node_random.inputs["Roughness"].default_value = 1.0
# node_random.inputs["Anisotropic"].default_value = 0.0
# node_random.inputs["Anisotropic Rotation"].default_value = 0.0
# node_random.inputs["IOR"].default_value = 1.450
# node_random.inputs["Alpha"].default_value = 0.555
# # create mix shader node
# node_mix = nodes.new(type="ShaderNodeMixShader")
# node_mix.location = 0, 0
# node_mix.inputs["Fac"].default_value = 0.079
# # create output node
# node_output = nodes.new(type="ShaderNodeOutputMaterial")
# node_output.location = 200, 0
# # link nodes
# links = mat.node_tree.links
# links.new(node_noise.outputs[1], node_HSV.inputs[4]) # link_noise_HSV
# links.new(node_HSV.outputs[0], node_random.inputs[0]) # link_HSV_random
# links.new(node_main.outputs[0], node_mix.inputs[1]) # link_main_mix
# links.new(node_random.outputs[0], node_mix.inputs[2]) # link_random_mix
# links.new(node_mix.outputs[0], node_output.inputs[0]) # link_mix_out
# return mat
# --- PHYSICS MODIFIER CONSTRUCTORS ---
[docs]
class PhysicsConstructor:
def __init__(self, *mod_contructors: "ModConstructor"):
self.mod_constructors = [*mod_contructors]
def __call__(self, obj: bpy.types.Object):
for mod_constructor in self.mod_constructors:
mod_constructor().construct(obj)
[docs]
class ModConstructor:
name = ""
type = ""
[docs]
def construct(self, obj):
mod = obj.modifiers.new(name=self.name, type=self.type)
self.setup_mod(mod)
[docs]
def setup_mod(self, mod: bpy.types.Modifier):
pass
[docs]
class ClothConstructor(ModConstructor):
name = "Cloth"
type = "CLOTH"
[docs]
def setup_mod(self, mod: bpy.types.ClothModifier):
stiffness = 1
pressure = 0.01
mod.settings.quality = 10
mod.settings.air_damping = 10
mod.settings.bending_model = "ANGULAR"
mod.settings.mass = 1
mod.settings.time_scale = 1
mod.point_cache.frame_start = bpy.context.scene.frame_start
mod.point_cache.frame_end = bpy.context.scene.frame_end
# Cloth > Stiffness
mod.settings.tension_stiffness = stiffness
mod.settings.compression_stiffness = stiffness
mod.settings.shear_stiffness = stiffness
mod.settings.bending_stiffness = 1
# Cloth > Damping
mod.settings.tension_damping = 50
mod.settings.compression_damping = 50
mod.settings.shear_damping = 50
mod.settings.bending_damping = 0.5
# Cloth > Pressure
mod.settings.use_pressure = True
mod.settings.uniform_pressure_force = pressure
mod.settings.use_pressure_volume = True
mod.settings.target_volume = 1
mod.settings.pressure_factor = 2
mod.settings.fluid_density = 1.05
# Cloth > Collisions
mod.collision_settings.collision_quality = 4
mod.collision_settings.use_collision = True
mod.collision_settings.use_self_collision = True
mod.collision_settings.self_friction = 0
mod.collision_settings.friction = 0
mod.collision_settings.self_distance_min = 0.01
mod.collision_settings.distance_min = 0.01
mod.collision_settings.self_impulse_clamp = 100
mod.collision_settings.impulse_clamp = 100
# Cloth > Field Weights
mod.settings.effector_weights.gravity = 0
[docs]
class YolkClothConstructor(ClothConstructor):
[docs]
def setup_mod(self, mod: bpy.types.ClothModifier):
stiffness = 5
pressure = 5.2
mod.settings.quality = 10
mod.settings.air_damping = 10
mod.settings.bending_model = "ANGULAR"
mod.settings.mass = 2
mod.settings.time_scale = 1
# Cloth > Stiffness
mod.settings.tension_stiffness = stiffness
mod.settings.compression_stiffness = stiffness
mod.settings.shear_stiffness = stiffness
mod.settings.bending_stiffness = stiffness
# Cloth > Damping
mod.settings.tension_damping = 50
mod.settings.compression_damping = 50
mod.settings.shear_damping = 50
mod.settings.bending_damping = 0.5
# Cloth > Internal Springs
mod.settings.use_internal_springs = True
mod.settings.internal_spring_max_length = 1
mod.settings.internal_spring_max_diversion = 0.785398
mod.settings.internal_spring_normal_check = False
mod.settings.internal_tension_stiffness = 10000
mod.settings.internal_compression_stiffness = 10000
mod.settings.internal_tension_stiffness_max = 10000
mod.settings.internal_compression_stiffness_max = 10000
# Cloth > Pressure
mod.settings.use_pressure = True
mod.settings.uniform_pressure_force = pressure
mod.settings.use_pressure_volume = True
mod.settings.target_volume = 1
mod.settings.pressure_factor = 2
mod.settings.fluid_density = 1.15
# Cloth > Collisions
mod.collision_settings.collision_quality = 10
mod.collision_settings.use_collision = True
mod.collision_settings.use_self_collision = True
mod.collision_settings.self_friction = 0
mod.collision_settings.friction = 0
mod.collision_settings.self_distance_min = 0.005
mod.collision_settings.distance_min = 0.005
mod.collision_settings.self_impulse_clamp = 0
[docs]
class CollisionConstructor(ModConstructor):
name = "Collision"
type = "COLLISION"
[docs]
def setup_mod(self, mod: bpy.types.CollisionModifier):
mod.settings.use_culling = True
mod.settings.damping = 1
mod.settings.thickness_outer = 0.025
mod.settings.thickness_inner = 0.25
mod.settings.cloth_friction = 0
mod.settings.use_normal = False
[docs]
class BoundaryCollisionConstructor(CollisionConstructor):
[docs]
def setup_mod(self, mod: bpy.types.CollisionModifier):
super(BoundaryCollisionConstructor, self).setup_mod(mod)
mod.settings.use_culling = False
[docs]
class SubsurfConstructor(ModConstructor):
name = "Subdivision"
type = "SUBSURF"
[docs]
def setup_mod(self, mod: bpy.types.SubsurfModifier):
mod.subdivision_type = "CATMULL_CLARK"
mod.levels = 1
mod.render_levels = 1
# not stable
[docs]
class RemeshConstructor(ModConstructor):
name = "Remesh"
type = "REMESH"
[docs]
def setup_mod(self, mod: bpy.types.RemeshModifier):
mod.mode = "VOXEL"
mod.voxel_size = 0.5
mod.adaptivity = 0
mod.use_remove_disconnected = True
mod.use_smooth_shade = True
mod.show_in_editmode = True