Files
blender-portable-repo/scripts/addons/goo_physics/functions.py
T
Nathan 6c3b78075b work: restore shift+spacebar for media play/pause
maybe put in maya config? idk what funiman's preference is
2026-05-29 14:58:59 -06:00

1530 lines
48 KiB
Python

from bpy.types import Panel, Menu, Operator, PropertyGroup
from bpy.props import *
from bpy.utils import register_classes_factory
from bpy_extras import anim_utils
import bpy
import os
import bmesh
from mathutils import Vector
import shutil
from pathlib import Path
import random
import addon_utils
import json
import numpy as np
#
# FUNCTIONS
#
def create_collider_object(
objects, ob_name, collide_coll, combine_ob_ng, convex_hull=True
):
mesh_dat = bpy.data.meshes.new(ob_name)
mesh_dat.from_pydata([], [], [])
mesh_dat.update()
collider_object = bpy.data.objects.new(ob_name, mesh_dat)
collide_coll.objects.link(collider_object)
collider_object.lock_location = [True, True, True]
collider_object.lock_scale = [True, True, True]
collider_object.lock_rotation = [True, True, True]
collider_object.hide_render = True
# collider_object.hide_viewport = True
mod = collider_object.modifiers.new("GP_Nodes Combine Objects", "NODES")
mod.node_group = combine_ob_ng
num = 0
for ob in objects:
set_geo_nodes_input(mod, "Object " + str((num % 20) + 1), ob)
num += 1
if (num % 20) == 0:
mod = collider_object.modifiers.new("GP_Nodes Combine Objects", "NODES")
mod.node_group = combine_ob_ng
set_geo_nodes_input(mod, "Convex Hull", convex_hull)
return collider_object
def create_sb_simple_mesh(name, b_len):
verts = [[0.0, 0.0, 0.0], [0.0, b_len, 0.0]]
edges = [[0, 1]]
mesh_dat = bpy.data.meshes.new(name)
mesh_dat.from_pydata(verts, edges, [])
mesh_dat.update()
ob = bpy.data.objects.new(name, mesh_dat)
vg = ob.vertex_groups.new(name="Head")
vg.add([0], 1.0, "REPLACE")
vg = ob.vertex_groups.new(name="Tail")
vg.add([1], 1.0, "REPLACE")
return ob
def create_jg_simple_mesh(name, b_len):
verts = [[0.0, 0.0, 0.0], [0.0, b_len, 0.0]]
edges = [[0, 1]]
mesh_dat = bpy.data.meshes.new(name)
mesh_dat.from_pydata(verts, edges, [])
mesh_dat.update()
ob = bpy.data.objects.new(name, mesh_dat)
vg = ob.vertex_groups.new(name="Head")
vg.add([0], 1.0, "REPLACE")
vg = ob.vertex_groups.new(name="Goal")
vg.add([0, 1], 1.0, "REPLACE")
return ob
def create_sb_coll_mesh(name, thickness, b_len, segments):
verts = []
faces = [[0, 1, 2, 3]]
for i in range(segments):
len_size = b_len * (i / (segments - 1))
seg_ind = i * 4
for ii in range(4):
x_val = -thickness if ii in [1, 2] else thickness
z_val = -thickness if ii in [2, 3] else thickness
verts.append([x_val, len_size, z_val])
if i < segments - 1:
if ii == 3:
faces.append(
[
seg_ind,
seg_ind + ii,
seg_ind + 4 + ii,
seg_ind + 4,
]
)
else:
faces.append(
[
seg_ind + 1 + ii,
seg_ind + ii,
seg_ind + 4 + ii,
seg_ind + 5 + ii,
]
)
faces.append([len(verts) - 1, len(verts) - 2, len(verts) - 3, len(verts) - 4])
mesh_dat = bpy.data.meshes.new(name)
mesh_dat.from_pydata(verts, [], faces)
mesh_dat.update()
ob = bpy.data.objects.new(name, mesh_dat)
vg = ob.vertex_groups.new(name="Head")
vg.add(faces[0], 1.0, "REPLACE")
vg = ob.vertex_groups.new(name="Tail")
vg.add(faces[-1], 1.0, "REPLACE")
return ob
def get_bone_chains(bones):
# Cache selected bones children checking for 1 level of constraint distance
valid_bones, child_bones = [], []
for pb in bones:
if pb.parent is None:
continue
children = []
for cpb in pb.children:
child_sel = get_bone_sel(cpb)
if child_sel and cpb in bones:
children.append(cpb)
valid_bones.append(pb)
child_bones.append(children)
dep_bones = child_bones.copy()
# Find all bones constrained to our valid bones
for ob in bpy.context.selected_objects:
if ob.type == "ARMATURE":
for pb in ob.pose.bones:
for con in pb.constraints:
if con.type in ["COPY_TRANSFORMS", "COPY_ROTATION", "COPY_LOCATION"]:
if con.target is None or con.target.type != "ARMATURE" or con.subtarget == "":
continue
if con.subtarget not in con.target.pose.bones:
continue
tar = con.target.pose.bones[con.subtarget]
if tar in valid_bones:
for cpb in pb.children:
child_sel = get_bone_sel(cpb)
if child_sel:
if len(child_bones[valid_bones.index(tar)]) == 0:
dep_bones[valid_bones.index(tar)].append(cpb)
# Get a list of all bones that are dependencies
chain_roots, is_dep = [], []
for d in dep_bones:
if len(d) == 1:
is_dep += d
else:
for dpb in d:
if dpb not in chain_roots:
chain_roots.append(dpb)
# Search through valid bones
for p, pb in enumerate(valid_bones):
if pb not in is_dep and pb not in chain_roots:
chain_roots.append(pb)
chain_parts = []
for pb in valid_bones:
if pb not in chain_roots:
chain_parts.append(pb)
chains, used_bones = [], []
for pb in chain_roots:
if pb.id_data.name + "_" + pb.name in used_bones:
continue
chain = [[pb.id_data, pb.name]]
used_bones.append(pb.id_data.name + "_" + pb.name)
for i in range(len(chain_parts)):
if pb in valid_bones:
deps = dep_bones[valid_bones.index(pb)]
if len(deps) == 1:
if deps[0] in chain_roots:
break
pb = deps[0]
if pb.id_data.name + "_" + pb.name in used_bones:
break
chain.append([pb.id_data, pb.name])
used_bones.append(pb.id_data.name + "_" + pb.name)
else:
break
else:
break
chains.append(chain[::-1])
return chains
def get_bone_chains_from_ids(bones, bone_ids):
# Cache selected bones children checking for 1 level of constraint distance
valid_bones, child_bones = [], []
for p, pb in enumerate(bones):
if pb.parent is None:
continue
children = []
for cpb in pb.children:
if cpb not in bones:
continue
if bone_ids[bones.index(cpb)] != bone_ids[p]:
continue
children.append(cpb)
valid_bones.append(pb)
child_bones.append(children)
dep_bones = child_bones.copy()
# Find all bones constrained to our valid bones
for ob in bpy.context.selected_objects:
if ob.type == "ARMATURE":
for pb in ob.pose.bones:
for con in pb.constraints:
if con.type in ["COPY_TRANSFORMS", "COPY_ROTATION", "COPY_LOCATION"]:
if con.target is None or con.target.type != "ARMATURE" or con.subtarget == "":
continue
if con.subtarget not in con.target.pose.bones:
continue
tar = con.target.pose.bones[con.subtarget]
if tar in valid_bones:
for cpb in pb.children:
if cpb not in bones:
continue
if bone_ids[bones.index(cpb)] != bone_ids[bones.index(pb)]:
continue
if len(child_bones[valid_bones.index(tar)]) == 0:
dep_bones[valid_bones.index(tar)].append(cpb)
# Get a list of all bones that are dependencies
chain_roots, is_dep = [], []
for d in dep_bones:
if len(d) == 1:
is_dep += d
else:
for dpb in d:
if dpb not in chain_roots:
chain_roots.append(dpb)
# Search through valid bones
for p, pb in enumerate(valid_bones):
if pb not in is_dep and pb not in chain_roots:
chain_roots.append(pb)
chain_parts = []
for pb in valid_bones:
if pb not in chain_roots:
chain_parts.append(pb)
chains, used_bones = [], []
for pb in chain_roots:
if pb.id_data.name + "_" + pb.name in used_bones:
continue
chain = [[pb.id_data, pb.name]]
used_bones.append(pb.id_data.name + "_" + pb.name)
for i in range(len(chain_parts)):
deps = dep_bones[valid_bones.index(pb)]
if len(deps) == 1:
if deps[0] in chain_roots:
break
pb = deps[0]
if pb.id_data.name + "_" + pb.name in used_bones:
break
chain.append([pb.id_data, pb.name])
used_bones.append(pb.id_data.name + "_" + pb.name)
else:
break
chains.append(chain[::-1])
return chains
def clear_sim_chains(bone_chains):
for chain in bone_chains:
for b, bo_dat in enumerate(chain):
rig = bo_dat[0]
pb = rig.pose.bones[bo_dat[1]]
track_cons = pb.constraints.get("GP_Track")
if track_cons:
pb.constraints.remove(track_cons)
follow_cons = pb.constraints.get("GP_Follow")
if follow_cons:
pb.constraints.remove(follow_cons)
ob = bpy.data.objects.get(pb.gp_sim_object)
if ob:
try:
mod = ob.modifiers.get("GP_Nodes Sim")
if mod:
root_ob = get_geo_nodes_input(mod, "Root Ref Object")
if root_ob is not None:
bpy.data.objects.remove(root_ob)
except:
pass
bpy.data.objects.remove(ob)
pb.gp_has_sb_physics = False
pb.gp_has_cl_physics = False
pb.gp_has_gn_physics = False
pb.gp_has_jg_physics = False
pb.gp_end_of_chain = False
pb.gp_chain_id = ""
pb.gp_sim_object = ""
return
def get_partial_selected_bone_chains(bones, geo_nodes_only=False, only_active=False, include_all=False):
chains, skip_bones = [], []
for pb in bones:
if pb.name in skip_bones:
continue
if only_active and bpy.context.active_pose_bone.gp_chain_id != pb.gp_chain_id:
continue
track_cons = pb.constraints.get("GP_Track")
follow_cons = pb.constraints.get("GP_Follow")
if follow_cons and pb.gp_has_jg_physics:
chain = [[pb.id_data, pb.name]]
skip_bones += [c[1] for c in chain]
chains.append(chain)
elif track_cons and (geo_nodes_only == False or pb.gp_has_gn_physics):
chain = [[pb.id_data, pb.name]]
for par in pb.parent_recursive:
if par.constraints.get("GP_Track"):
chain.insert(0, [par.id_data, par.name])
if par.gp_end_of_chain:
break
else:
break
searching = True
search_pb = pb
while searching:
search_len = len(chain)
for child in search_pb.children:
if child.constraints.get("GP_Track"):
if child.gp_end_of_chain == False:
chain.append([child.id_data, child.name])
search_pb = child
break
else:
break
searching = len(chain) != search_len
skip_bones += [c[1] for c in chain]
chains.append(chain)
else:
if include_all:
chains.append([[pb.id_data, pb.name]])
return chains
def create_geonodes_chains(chains, coll, collide_colls, divisions):
prefs = bpy.context.preferences.addons["goo_physics"].preferences
link_fp = get_nodes_fp()
# Link in geo nodes node groups
simulation_ng = bpy.data.node_groups.get("GooPhysics_SimulationProcess")
if simulation_ng is None:
with bpy.data.libraries.load(
str(link_fp), relative=True
) as (data_from, data_to):
data_to.node_groups = [
nt
for nt in data_from.node_groups
if nt in ["GooPhysics_SimulationProcess"]
]
simulation_ng = data_to.node_groups[0]
coll_ob_ng = bpy.data.node_groups.get("GooPhysics_CollisionCreate")
if coll_ob_ng is None:
with bpy.data.libraries.load(
str(link_fp), relative=True
) as (data_from, data_to):
data_to.node_groups = [
nt
for nt in data_from.node_groups
if nt in ["GooPhysics_CollisionCreate"]
]
coll_ob_ng = data_to.node_groups[0]
comb_ob_ng = bpy.data.node_groups.get("GooPhysics_CombineObjects")
if comb_ob_ng is None:
with bpy.data.libraries.load(
str(link_fp), relative=True
) as (data_from, data_to):
data_to.node_groups = [
nt
for nt in data_from.node_groups
if nt in ["GooPhysics_CombineObjects"]
]
comb_ob_ng = data_to.node_groups[0]
#
chain_obs = []
for c, chain in enumerate(chains):
next_num = c
for ob in coll.objects:
try:
cur_num = int(
ob.name.replace("GP_GeoNodes_Chain_Root_Ref_", "")
.replace("GP_GeoNodes_Chain_", "")
.split("_")[0]
)
if next_num < cur_num:
next_num = cur_num
except:
pass
num = "%02d" % (next_num + 1)
verts, edges = [], []
for cc, c_dat in enumerate(chain[::-1]):
rig = c_dat[0]
bone = rig.pose.bones[c_dat[1]]
bone.gp_has_gn_physics = True
bone.gp_chain_id = num
head_co = bone.head
tail_co = bone.tail
vec = tail_co - head_co
if cc == 0:
verts.append(head_co)
for i in range(divisions):
fac = (i + 1) / (divisions + 1)
verts.append(head_co + vec * fac)
edges.append([len(verts) - 2, len(verts) - 1])
verts.append(tail_co)
edges.append([len(verts) - 2, len(verts) - 1])
bone.gp_end_of_chain = cc == 0
#
rig = chain[-1][0]
bone = rig.pose.bones[chain[-1][1]]
chain_name = "GP_GeoNodes_Chain_" + num
mesh_dat = bpy.data.meshes.new(chain_name)
mesh_dat.from_pydata(verts, edges, [])
mesh_dat.update()
chain_ob = bpy.data.objects.new(chain_name, mesh_dat)
coll.objects.link(chain_ob)
chain_root_ref = bpy.data.objects.new(
chain_name.replace("_Chain_", "_Chain_Root_Ref_"), None
)
coll.objects.link(chain_root_ref)
chain_root_ref.empty_display_size = 0.025
chain_root_ref.empty_display_type = "CUBE"
for cc, c_dat in enumerate(chain[::-1]):
rig = c_dat[0]
bone = rig.pose.bones[c_dat[1]]
c_ob = chain_ob.gp_bone_coll.coll.add()
c_ob.rig_object = rig.name
c_ob.rig_bone = c_dat[1]
chain_ob.lock_location = [True, True, True]
chain_ob.lock_scale = [True, True, True]
chain_ob.lock_rotation = [True, True, True]
chain_ob.matrix_world = rig.matrix_world
chain_ob.parent = rig
hook = chain_ob.modifiers.new("GP_Root Hook", "HOOK")
mod = chain_ob.modifiers.new("GP_Nodes Sim", "NODES")
mod.show_viewport = prefs.gp_physics_active
mod.node_group = simulation_ng
set_geo_nodes_input(mod, "Root Ref Object", chain_root_ref)
set_geo_nodes_input(mod, "Seed", random.randint(-5000, 5000))
if bone.gp_chain_limit_collision and prefs.keep_existing_settings:
set_geo_nodes_input(mod, "Collision Collection", collide_colls[4])
else:
set_geo_nodes_input(mod, "Collision Collection", collide_colls[0])
set_geo_nodes_input(mod, "Use Collision", True)
#
for cc, c_dat in enumerate(chain[::-1]):
rig = c_dat[0]
bone = rig.pose.bones[c_dat[1]]
if cc == 0:
set_geo_nodes_input(mod, "Velocity Scaler", bone.gp_chain_velocity)
set_geo_nodes_input(mod, "Velocity Dampening", bone.gp_chain_dampening)
set_geo_nodes_input(mod, "Gravity Strength", bone.gp_chain_gravity)
set_geo_nodes_input(mod, "Root Pin Size", bone.gp_chain_root_falloff)
set_geo_nodes_input(mod, "Goal Strength", bone.gp_chain_stiffness)
set_geo_nodes_input(mod, "Goal End Strength", bone.gp_chain_stiff_end_fac)
set_geo_nodes_input(mod, "Goal Velocity Factor", bone.gp_chain_stiff_vel_fac)
set_geo_nodes_input(mod, "Goal Velocity Min", bone.gp_chain_stiff_vel_min)
set_geo_nodes_input(mod, "Goal Velocity Max", bone.gp_chain_stiff_vel_max)
set_geo_nodes_input(mod, "Wind Strength", bone.gp_chain_wind_strength)
set_geo_nodes_input(mod, "Wind Noise Strength", bone.gp_chain_wind_noise_strength)
set_geo_nodes_input(mod, "Wind Noise Scale", bone.gp_chain_wind_noise_scale)
set_geo_nodes_input(mod, "Collision Distance", bone.gp_chain_collision_dist)
set_geo_nodes_input(mod, "Collision Friction", bone.gp_chain_collision_friction)
# chain_root_ref.parent = rig
# chain_root_ref.parent_type = "BONE"
# chain_root_ref.parent_bone = bone.parent.name
# chain_root_ref.matrix_world = rig.matrix_world @ bone.parent.matrix
# chain_root_ref.location = chain_root_ref.matrix_world.inverted() @ (
# rig.matrix_world @ bone.head
# )
# chain_root_ref.location[1] -= bone.parent.length
# chain_root_ref.scale = [1.0, 1.0, 1.0]
# chain_root_ref.rotation_euler = [0.0, 0.0, 0.0]
chain_root_ref.lock_location = [True, True, True]
chain_root_ref.lock_rotation = [True, True, True]
chain_root_ref.lock_scale = [True, True, True]
mat = rig.matrix_world @ bone.matrix
chain_root_ref.matrix_world = mat
con = chain_root_ref.constraints.new("CHILD_OF")
con.target = rig
con.subtarget = bone.parent.name
# con = chain_root_ref.constraints.new("COPY_TRANSFORMS")
# con.target = rig
# con.subtarget = bone.name
vg = chain_ob.vertex_groups.new(name="Root")
vg.add([0], 1.0, "REPLACE")
vg_name = "Bone_" + str(cc + 1)
vg = chain_ob.vertex_groups.new(name=vg_name)
vg.add([cc + divisions * (cc + 1) + 1], 1.0, "REPLACE")
bone.gp_sim_object = chain_ob.name
con = bone.constraints.new("DAMPED_TRACK")
con.name = "GP_Track"
con.target = chain_ob
con.subtarget = vg_name
# con.influence = 1.0
con.influence = bone.gp_sim_influence
con.enabled = prefs.gp_physics_active
vg = chain_ob.vertex_groups.new(name="All")
vg.add([i for i in range(len(chain_ob.data.vertices))], 1.0, "REPLACE")
hook.matrix_inverse = mat.inverted()
hook.object = chain_root_ref
hook.vertex_group = "All"
chain_obs.append(chain_ob)
return chain_obs
def create_soft_chains(chains, coll, collide_colls):
prefs = bpy.context.preferences.addons["goo_physics"].preferences
chain_sets = []
for c, chain in enumerate(chains):
next_num = c
for ob in coll.objects:
try:
cur_num = int(
ob.name.replace("GP_SoftBody_Chain_Root_Ref_", "")
.replace("GP_SoftBody_Chain_", "")
.split("_")[0]
)
if next_num < cur_num:
next_num = cur_num
except:
pass
num = "%02d" % (next_num + 1)
chain_obs = []
for cc, c_dat in enumerate(chain[::-1]):
b_ind = "%02d" % (cc + 1)
rig = c_dat[0]
bone = rig.pose.bones[c_dat[1]]
bone.gp_has_sb_physics = True
bone.gp_chain_id = num
chain_name = "GP_SoftBody_Chain_" + num + "_" + b_ind
chain_ob = create_sb_simple_mesh(
chain_name,
bone.length,
)
coll.objects.link(chain_ob)
bone.gp_end_of_chain = cc == 0
bone.gp_sim_object = chain_ob.name
c_ob = chain_ob.gp_bone_coll.coll.add()
c_ob.rig_object = c_dat[0].name
c_ob.rig_bone = c_dat[1]
chain_obs.append(chain_ob)
con = bone.constraints.new("DAMPED_TRACK")
con.name = "GP_Track"
con.target = chain_ob
con.subtarget = "Tail"
con.influence = bone.gp_sim_influence
con.enabled = prefs.gp_physics_active
chain_ob.parent = rig
chain_ob.parent_type = "BONE"
chain_ob.parent_bone = bone.parent.name
chain_ob.matrix_world = rig.matrix_world @ bone.matrix
chain_ob.lock_location = [True, True, True]
chain_ob.lock_scale = [True, True, True]
chain_ob.lock_rotation = [True, True, True]
mod = chain_ob.modifiers.new("GP_SoftBody Sim", "SOFT_BODY")
mod.show_viewport = prefs.gp_physics_active
#
if bpy.context.scene.use_preview_range:
mod.point_cache.frame_start = bpy.context.scene.frame_preview_start
mod.point_cache.frame_end = bpy.context.scene.frame_preview_end
else:
mod.point_cache.frame_start = bpy.context.scene.frame_start
mod.point_cache.frame_end = bpy.context.scene.frame_end
mod.settings.use_edge_collision = True
mod.settings.bend = 10
mod.settings.pull = 1.0
mod.settings.push = 1.0
mod.settings.step_min = 100
mod.settings.choke = 0
mod.settings.fuzzy = 0
mod.settings.spring_length = 100
mod.settings.vertex_group_goal = "Head"
mod.settings.goal_spring = bone.gp_sim_stiffness
mod.settings.goal_friction = bone.gp_sim_damping
mod.settings.goal_default = 1.0
mod.settings.goal_min = bone.gp_sim_strength
mod.settings.goal_max = 1.0
mod.settings.effector_weights.gravity = bone.gp_sim_gravity
mod.settings.speed = bone.gp_sim_speed
mod.settings.friction = bone.gp_sim_friction
mod.settings.mass = bone.gp_sim_mass
if bone.gp_chain_limit_collision and prefs.keep_existing_settings:
mod.settings.collision_collection = collide_colls[2]
else:
mod.settings.collision_collection = None
#
chain_sets.append(chain_obs)
return chain_sets
def create_spring_chains(chains, coll, collide_colls):
prefs = bpy.context.preferences.addons["goo_physics"].preferences
chain_sets = []
for c, chain in enumerate(chains):
next_num = c
for ob in coll.objects:
try:
cur_num = int(
ob.name.replace("GP_Jiggle_Root_Ref_", "")
.replace("GP_Jiggle_", "")
.split("_")[0]
)
if next_num < cur_num:
next_num = cur_num
except:
pass
num = "%02d" % (next_num + 1)
chain_obs = []
for cc, c_dat in enumerate(chain[::-1]):
b_ind = "%02d" % (cc + 1)
rig = c_dat[0]
bone = rig.pose.bones[c_dat[1]]
bone.gp_has_jg_physics = True
bone.gp_chain_id = num
chain_name = "GP_Jiggle_" + num + "_" + b_ind
chain_ob = create_jg_simple_mesh(
chain_name,
bone.length,
)
coll.objects.link(chain_ob)
bone.gp_end_of_chain = cc == 0
bone.gp_sim_object = chain_ob.name
c_ob = chain_ob.gp_bone_coll.coll.add()
c_ob.rig_object = c_dat[0].name
c_ob.rig_bone = c_dat[1]
chain_obs.append(chain_ob)
con = bone.constraints.new("COPY_LOCATION")
con.name = "GP_Follow"
con.target = chain_ob
con.subtarget = "Head"
con.influence = bone.gp_sim_influence
con.enabled = prefs.gp_physics_active
chain_ob.parent = rig
chain_ob.parent_type = "BONE"
chain_ob.parent_bone = bone.parent.name
chain_ob.matrix_world = rig.matrix_world @ bone.matrix
chain_ob.lock_location = [True, True, True]
chain_ob.lock_scale = [True, True, True]
chain_ob.lock_rotation = [True, True, True]
mod = chain_ob.modifiers.new("GP_Jiggle Sim", "SOFT_BODY")
mod.show_viewport = prefs.gp_physics_active
#
if bpy.context.scene.use_preview_range:
mod.point_cache.frame_start = bpy.context.scene.frame_preview_start
mod.point_cache.frame_end = bpy.context.scene.frame_preview_end
else:
mod.point_cache.frame_start = bpy.context.scene.frame_start
mod.point_cache.frame_end = bpy.context.scene.frame_end
mod.settings.use_edge_collision = True
mod.settings.bend = 10
mod.settings.pull = 1.0
mod.settings.push = 1.0
mod.settings.step_min = 100
mod.settings.choke = 0
mod.settings.fuzzy = 0
mod.settings.spring_length = 100
mod.settings.vertex_group_goal = "Goal"
mod.settings.goal_spring = bone.gp_sim_stiffness
mod.settings.goal_friction = bone.gp_sim_damping
mod.settings.goal_default = 1.0
mod.settings.goal_min = 0.99
mod.settings.goal_max = 0.99
mod.settings.effector_weights.gravity = bone.gp_sim_gravity
mod.settings.speed = bone.gp_sim_speed
mod.settings.friction = bone.gp_sim_friction
mod.settings.mass = bone.gp_sim_mass
if bone.gp_chain_limit_collision and prefs.keep_existing_settings:
mod.settings.collision_collection = collide_colls[1]
else:
mod.settings.collision_collection = None
#
chain_sets.append(chain_obs)
return chain_sets
def create_cloth_chains(chains, coll, collide_colls):
prefs = bpy.context.preferences.addons["goo_physics"].preferences
thick = .001
chain_obs = []
for c, chain in enumerate(chains):
next_num = c
for ob in coll.objects:
try:
cur_num = int(
ob.name.replace("GP_Cloth_Chain_Root_Ref_", "")
.replace("GP_Cloth_Chain_", "")
.split("_")[0]
)
if next_num < cur_num:
next_num = cur_num
except:
pass
num = "%02d" % (next_num + 1)
verts, faces = [], []
for cc, c_dat in enumerate(chain[::-1]):
rig = c_dat[0]
bone = rig.pose.bones[c_dat[1]]
bone.gp_has_cl_physics = True
bone.gp_chain_id = num
mat = rig.matrix_world @ bone.matrix
if cc == 0:
verts.append(mat @ Vector((thick, 0.0, thick)))
verts.append(mat @ Vector((-thick, 0.0, thick)))
verts.append(mat @ Vector((-thick, 0.0, -thick)))
verts.append(mat @ Vector((thick, 0.0, -thick)))
faces.append([0, 1, 2, 3])
else:
verts[-4] = (verts[-4] + mat @ Vector((thick, 0.0, thick))) * 0.5
verts[-3] = (verts[-3] + mat @ Vector((-thick, 0.0, thick))) * 0.5
verts[-2] = (verts[-2] + mat @ Vector((-thick, 0.0, -thick))) * 0.5
verts[-1] = (verts[-1] + mat @ Vector((thick, 0.0, -thick))) * 0.5
ind = len(verts) - 4
verts.append(mat @ Vector((thick, bone.length, thick)))
verts.append(mat @ Vector((-thick, bone.length, thick)))
verts.append(mat @ Vector((-thick, bone.length, -thick)))
verts.append(mat @ Vector((thick, bone.length, -thick)))
faces.append([ind, ind + 4, ind + 5, ind + 1])
faces.append([ind + 1, ind + 5, ind + 6, ind + 2])
faces.append([ind + 2, ind + 6, ind + 7, ind + 3])
faces.append([ind + 3, ind + 7, ind + 4, ind])
if cc == len(chain) - 1:
ind = len(verts) - 4
faces.append([ind, ind + 1, ind + 2, ind + 3])
bone.gp_end_of_chain = cc == 0
#
rig = chain[-1][0]
bone = rig.pose.bones[chain[-1][1]]
chain_name = "GP_Cloth_Chain_" + num
mesh_dat = bpy.data.meshes.new(chain_name)
mesh_dat.from_pydata(verts, [], faces)
mesh_dat.update()
chain_ob = bpy.data.objects.new(chain_name, mesh_dat)
coll.objects.link(chain_ob)
for cc, c_dat in enumerate(chain[::-1]):
rig = c_dat[0]
bone = rig.pose.bones[c_dat[1]]
c_ob = chain_ob.gp_bone_coll.coll.add()
c_ob.rig_object = rig.name
c_ob.rig_bone = c_dat[1]
chain_ob.lock_location = [True, True, True]
chain_ob.lock_scale = [True, True, True]
chain_ob.lock_rotation = [True, True, True]
mod = chain_ob.modifiers.new("GP_Cloth Sim", "CLOTH")
mod.show_viewport = prefs.gp_physics_active
#
if bpy.context.scene.use_preview_range:
mod.point_cache.frame_start = bpy.context.scene.frame_preview_start
mod.point_cache.frame_end = bpy.context.scene.frame_preview_end
else:
mod.point_cache.frame_start = bpy.context.scene.frame_start
mod.point_cache.frame_end = bpy.context.scene.frame_end
mod.settings.quality = 12
mod.settings.mass = bone.gp_chain_mass
mod.settings.bending_model = "LINEAR"
mod.settings.use_pressure = True
mod.settings.uniform_pressure_force = 1.0
mod.settings.pressure_factor = 0.25
mod.settings.air_damping = bone.gp_chain_air_dampening
mod.settings.tension_stiffness = 15.0
mod.settings.shear_stiffness = 5.0
mod.settings.bending_stiffness = 1.0
mod.settings.tension_damping = 5.0
mod.settings.shear_damping = 5.0
mod.settings.bending_damping = bone.gp_chain_bend_damping
mod.settings.effector_weights.gravity = bone.gp_chain_gravity
mod.settings.time_scale = bone.gp_chain_speed
mod.collision_settings.use_collision = True
mod.collision_settings.distance_min = bone.gp_chain_collision_dist
if bone.gp_chain_limit_collision and prefs.keep_existing_settings:
mod.collision_settings.collection = collide_colls[3]
else:
mod.collision_settings.collection = None
pg_name = "Pin"
pin_group = chain_ob.vertex_groups.new(name=pg_name)
mod.settings.vertex_group_mass = pg_name
#
end_cnt = round(len(chain) * 0.25)
if end_cnt == 0:
end_cnt += 1
fac = 1.0 / end_cnt
for cc, c_dat in enumerate(chain[::-1]):
rig = c_dat[0]
bone = rig.pose.bones[c_dat[1]]
if cc == 0:
mat = chain_ob.matrix_world.copy()
chain_ob.parent = rig
chain_ob.parent_type = "BONE"
chain_ob.parent_bone = bone.parent.name
mat = bone.parent.matrix.copy()
mat[0][3] = mat.translation[0] + bone.parent.y_axis[0] * bone.parent.length
mat[1][3] = mat.translation[1] + bone.parent.y_axis[1] * bone.parent.length
mat[2][3] = mat.translation[2] + bone.parent.y_axis[2] * bone.parent.length
mat = rig.matrix_world @ mat
chain_ob.data.transform(mat.inverted())
# chain_ob.matrix_world = mat
# chain_ob.matrix_parent_inverse = chain_ob.matrix_basis
chain_ob.location = [0.0, 0.0, 0.0]
chain_ob.scale = [1.0, 1.0, 1.0]
chain_ob.rotation_euler = [0.0, 0.0, 0.0]
vg = chain_ob.vertex_groups.new(name="Root")
vg.add([0, 1, 2, 3], 1.0, "REPLACE")
pin_group.add([0, 1, 2, 3], 1.0, "REPLACE")
vg_name = "Bone_" + str(cc + 1)
vg = chain_ob.vertex_groups.new(name=vg_name)
vg.add(
[(cc + 1) * 4, (cc + 1) * 4 + 1, (cc + 1) * 4 + 2, (cc + 1) * 4 + 3],
1.0,
"REPLACE",
)
pin_group.add(
[cc * 4, cc * 4 + 1, cc * 4 + 2, cc * 4 + 3],
1.0 - (cc * fac),
"REPLACE",
)
bone.gp_sim_object = chain_ob.name
con = bone.constraints.new("DAMPED_TRACK")
con.name = "GP_Track"
con.target = chain_ob
con.subtarget = vg_name
con.influence = bone.gp_sim_influence
con.enabled = prefs.gp_physics_active
chain_obs.append(chain_ob)
return chain_obs
def get_preset_data(preset_override=None):
prefs = bpy.context.preferences.addons["goo_physics"].preferences
preset_fp, user_preset_fp = None, None
bone_has_settings = False
pb = bpy.context.active_pose_bone
presets_enum = None
if pb.gp_has_cl_physics:
preset_fp = Path(os.path.dirname(__file__)) / "presets" / "cloth_presets.json"
user_preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "cloth_presets.json"
presets_enum = prefs.cl_presets
bone_has_settings = "gp_chain_air_dampening" in pb
elif pb.gp_has_sb_physics:
preset_fp = Path(os.path.dirname(__file__)) / "presets" / "soft_body_presets.json"
user_preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "soft_body_presets.json"
presets_enum = prefs.sb_presets
bone_has_settings = "gp_sim_strength" in pb
elif pb.gp_has_jg_physics:
preset_fp = Path(os.path.dirname(__file__)) / "presets" / "jiggle_spring_presets.json"
user_preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "jiggle_spring_presets.json"
presets_enum = prefs.jg_presets
bone_has_settings = "gp_sim_friction" in pb
elif pb.gp_has_gn_physics:
preset_fp = Path(os.path.dirname(__file__)) / "presets" / "geo_nodes_presets.json"
user_preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "geo_nodes_presets.json"
presets_enum = prefs.gn_presets
bone_has_settings = "gp_chain_root_falloff" in pb
if preset_override is not None:
presets_enum = preset_override
user_presets_data = {}
if user_preset_fp is not None and os.path.exists(user_preset_fp):
user_presets_data = json.load(open(str(user_preset_fp)))
base_presets_data = {}
if preset_fp is not None and os.path.exists(preset_fp):
base_presets_data = json.load(open(str(preset_fp)))
presets_data = base_presets_data | user_presets_data
if presets_enum is not None and presets_enum.replace(" ", "").upper() in presets_data.keys():
p_data = presets_data[presets_enum.replace(" ", "").upper()]["Settings"]
if prefs.keep_existing_settings and bone_has_settings:
for k in p_data.keys():
if k in pb:
p_data[k] = pb[k]
return p_data
return None
def get_bone_map_preset_data():
prefs = bpy.context.preferences.addons["goo_physics"].preferences
user_preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "bone_map_presets.json"
presets_enum = prefs.bm_presets
user_presets_data = {}
if user_preset_fp is not None and os.path.exists(user_preset_fp):
user_presets_data = json.load(open(str(user_preset_fp)))
if presets_enum.replace(" ", "").upper() in user_presets_data.keys():
p_data = user_presets_data[presets_enum.replace(" ", "").upper()]["Chain Settings"]
return p_data
return None
def apply_preset(preset_data, bone):
prefs = bpy.context.preferences.addons["goo_physics"].preferences
if prefs.apply_to_all_chains:
sim_chains = get_partial_selected_bone_chains(bpy.context.selected_pose_bones)
else:
sim_chains = get_partial_selected_bone_chains(bpy.context.selected_pose_bones_from_active_object, only_active=True)
for s in preset_data.keys():
if prefs.keep_existing_settings and s in bone:
continue
if isinstance(preset_data[s], list) == False:
if hasattr(bone, s):
setattr(bone, s, preset_data[s])
else:
vals = np.array(preset_data[s], dtype=np.float32)
source_t_vals = np.arange(vals.shape[0], dtype=np.float32) / (vals.shape[0] - 1)
for chain in sim_chains:
t_vals = np.arange(len(chain), dtype=np.float32) / (len(chain) - 1)
tar_inds = np.searchsorted(source_t_vals, t_vals)
p_inds = tar_inds - 1
tar_inds[0] = 1
p_inds[0] = 0
l_range = source_t_vals[tar_inds] - source_t_vals[p_inds]
l_fac = (t_vals - source_t_vals[p_inds]) / l_range
lerp_vals = vals[p_inds] + (vals[tar_inds] - vals[p_inds]) * l_fac
lerp_vals[0] = vals[0]
lerp_vals[-1] = vals[-1]
for b, b_dat in enumerate(chain):
bo = b_dat[0].pose.bones[b_dat[1]]
if hasattr(bo, s):
bo.gp_update = False
setattr(bo, s, lerp_vals[b])
bo.gp_update = True
return
def apply_bone_map_preset(preset_data, coll, collide_colls):
prefs = bpy.context.preferences.addons["goo_physics"].preferences
rig = bpy.context.active_object
for pb in rig.pose.bones:
pb.gp_chain_id = ""
missing_bones, existing_bones, existing_ids, existing_types = [], [], [], []
for b_dat in preset_data:
pb = rig.pose.bones.get(b_dat["Bone Name"])
if pb is None:
missing_bones.append(b_dat["Bone Name"])
else:
existing_bones.append(pb)
existing_ids.append(b_dat["Chain ID"])
existing_types.append(b_dat["Chain Type"])
sim_chains = get_bone_chains_from_ids(existing_bones, existing_ids)
spring_chains, cloth_chains, soft_chains, geo_chains = [], [], [], []
for c, chain in enumerate(sim_chains):
pb = chain[0][0].pose.bones[chain[0][1]]
chain_type = existing_types[existing_bones.index(pb)]
if chain_type == "GEO_NODES":
geo_chains.append(chain)
elif chain_type == "SOFT_BODY":
soft_chains.append(chain)
elif chain_type == "CLOTH":
cloth_chains.append(chain)
elif chain_type == "JIGGLE":
spring_chains.append(chain)
if len(spring_chains) > 0:
sim_spring_obs = create_spring_chains(spring_chains, coll, collide_colls)
if len(cloth_chains) > 0:
sim_cloth_obs = create_cloth_chains(cloth_chains, coll, collide_colls)
if len(soft_chains) > 0:
sim_soft_obs = create_soft_chains(soft_chains, coll, collide_colls)
if len(geo_chains) > 0:
sim_soft_obs = create_geonodes_chains(
geo_chains, coll, collide_colls, prefs.gp_divisions
)
return missing_bones
def get_falloff(value):
return 1 - (value * value)
def get_inputs_container(node_group):
if bpy.app.version[0] >= 4:
input_container = node_group.interface.items_tree
else:
input_container = node_group.inputs
return input_container
def is_addon_enabled(addon_name, version=None):
modules = addon_utils.modules()
for module in modules:
module_name = module.__name__
if module_name == addon_name:
is_enabled = addon_utils.check(module_name)[1]
if is_enabled and version is not None:
return module.bl_info["version"] >= version
else:
return is_enabled
return False
def is_addon_installed(addon_name):
for module in addon_utils.modules():
if addon_name == module.__name__:
return True
return False
def create_collider_coll(name, par_coll):
c_coll = bpy.data.collections.get(name)
if c_coll is None or c_coll.library is not None or c_coll.override_library is not None:
c_coll = bpy.data.collections.new(name)
c_coll.hide_render = True
if c_coll.name not in par_coll.children:
par_coll.children.link(c_coll)
return c_coll
def ensure_collections():
coll = bpy.data.collections.get("Goo Physics")
if coll is None or coll.library is not None:
coll = bpy.data.collections.new("Goo Physics")
coll.hide_render = True
if "Shot" in bpy.context.scene.collection.children:
bpy.context.scene.collection.children["Shot"].children.link(coll)
else:
bpy.context.scene.collection.children.link(coll)
collide_coll = create_collider_coll("Goo Physics All Colliders", coll)
sb_collide_coll = create_collider_coll("Goo Physics Soft Body Colliders", collide_coll)
jg_collide_coll = create_collider_coll("Goo Physics Jiggle Colliders", collide_coll)
cl_collide_coll = create_collider_coll("Goo Physics Cloth Colliders", collide_coll)
gn_collide_coll = create_collider_coll("Goo Physics Geo Nodes Colliders", collide_coll)
controller_coll = create_collider_coll("Goo Physics Controllers", coll)
bake_coll = create_collider_coll("Goo Physics Bakes", coll)
return coll, [collide_coll, sb_collide_coll, jg_collide_coll, cl_collide_coll, gn_collide_coll], controller_coll, bake_coll
def get_nodes_fp():
if bpy.app.version[0] < 4:
link_fp = Path(os.path.dirname(__file__)) / "gp_sim_nodes_tree_3_6.blend"
else:
link_fp = Path(os.path.dirname(__file__)) / "gp_sim_nodes_tree.blend"
return link_fp
def clean_action_of_props(action):
if bpy.app.version[0] >= 5:
fcs = []
for slot in action.slots:
fcs.append(anim_utils.action_get_channelbag_for_slot(action, slot))
else:
fcs = [action]
props = ["gp_end_of_chain",
"gp_update",
"gp_sim_",
"gp_chain_",
"gp_has_"]
for slot in fcs:
rem_curves = []
for fc in slot.fcurves:
if (any([p in fc.data_path for p in props])):
rem_curves.append(fc)
for fc in rem_curves:
slot.fcurves.remove(fc)
return
def set_geo_nodes_input(modifier, input_name, setting):
inputs = get_inputs_container(modifier.node_group)
if bpy.app.version[0] < 5 or (bpy.app.version[0] == 5 and bpy.app.version[1] < 2):
modifier[inputs[input_name].identifier] = setting
else:
prop_attr = getattr(modifier.properties.inputs, inputs[input_name].identifier)
prop_attr.value = setting
return
def get_geo_nodes_input(modifier, input_name):
inputs = get_inputs_container(modifier.node_group)
if bpy.app.version[0] < 5 or (bpy.app.version[0] == 5 and bpy.app.version[1] < 2):
input_val = modifier[inputs[input_name].identifier]
else:
input_val = getattr(modifier.properties.inputs, inputs[input_name].identifier).value
return input_val
def get_geo_nodes_input_str(modifier, input_name):
inputs = get_inputs_container(modifier.node_group)
if bpy.app.version[0] < 5 or (bpy.app.version[0] == 5 and bpy.app.version[1] < 2):
input_container = modifier
input_str = '["' + inputs[input_name].identifier + '"]'
else:
input_str = "value"
input_container = getattr(modifier.properties.inputs, inputs[input_name].identifier)
return input_container, input_str
def get_bone_sel(bone):
if bpy.app.version[0] >= 5:
return bone.select
else:
return bone.bone.select
def set_bone_sel(bone, status):
if bpy.app.version[0] >= 5:
bone.select = status
else:
bone.bone.select = status
return
def smart_bake_pose(obj, use_existing=False, frame_start=1, frame_end=250, **kwargs):
act = getattr(obj.animation_data, "action", None)
if act is None:
return
keyed_frames = set()
action = get_action_slots(act)
for curve in action[0].fcurves:
frame_nums = [0.0 for i in range(len(curve.keyframe_points) * 2)]
curve.keyframe_points.foreach_get("co", frame_nums)
for fnum in frame_nums[::2]:
keyed_frames.add(int(fnum))
frames = sorted(filter(lambda x: frame_start <= x <= frame_end, keyed_frames))
if bpy.app.version[0] >= 5 or (bpy.app.version[0] == 4 and bpy.app.version[1] >= 1):
bake_opts = anim_utils.BakeOptions(
only_selected=kwargs["only_selected"],
do_pose=kwargs["do_pose"],
do_object=kwargs["do_object"],
do_visual_keying=kwargs["do_visual_keying"],
do_constraint_clear=kwargs["do_constraint_clear"],
do_parents_clear=kwargs["do_parents_clear"],
do_clean=kwargs["do_clean"],
do_location=True,
do_rotation=True,
do_scale=True,
do_bbone=False,
do_custom_props=False,
)
anim_utils.bake_action(
obj,
action=act if use_existing else None,
frames=frames,
bake_options=bake_opts,
)
else:
anim_utils.bake_action(
obj, action=act if use_existing else None, frames=frames, **kwargs
)
#
# HANDLERS
#
def timeline_sync_handler_frame_post(scene):
prefs = bpy.context.preferences.addons["goo_physics"].preferences
prefs.active_timeline_sync_frames += "," + str(scene.frame_current)
frame_start = scene.frame_start
if scene.use_preview_range:
frame_start = scene.frame_preview_start
if scene.frame_current == frame_start:
prefs.timeline_sync_frames = prefs.active_timeline_sync_frames
prefs.active_timeline_sync_frames = str(frame_start)
if prefs.precaching:
bpy.ops.screen.animation_cancel()
prefs.precaching = False
return
def timeline_sync_handler_playback_pre(scene):
prefs = bpy.context.preferences.addons["goo_physics"].preferences
frame_start = scene.frame_start
if scene.use_preview_range:
frame_start = scene.frame_preview_start
prefs.active_timeline_sync_frames = str(frame_start)
if scene.frame_current != frame_start:
prefs.active_timeline_sync_frames += "," + str(scene.frame_current)
bpy.app.handlers.frame_change_post.append(timeline_sync_handler_frame_post)
return
def timeline_sync_handler_playback_post(scene):
bpy.app.handlers.frame_change_post[:] = [h for h in bpy.app.handlers.frame_change_post if h.__name__ != "timeline_sync_handler_frame_post"]
return