6c3b78075b
maybe put in maya config? idk what funiman's preference is
1753 lines
61 KiB
Python
1753 lines
61 KiB
Python
from .functions import *
|
|
|
|
|
|
#
|
|
# OPERATORS
|
|
#
|
|
|
|
|
|
class GP_OT_refresh_collision_collection(Operator):
|
|
bl_idname = "goophys.refresh_collision_collection"
|
|
bl_label = "Refresh Collision Objects"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Moves all collision enabled objects into the collider collections"
|
|
|
|
def execute(self, context):
|
|
coll, collide_colls, control_coll, bake_coll = ensure_collections()
|
|
|
|
add_obs = []
|
|
for ob in context.view_layer.objects:
|
|
if ob.type in ["MESH", "CURVES"]:
|
|
for mod in ob.modifiers:
|
|
if mod.type == "COLLISION":
|
|
add_obs.append(ob)
|
|
|
|
for i in range(len(collide_colls[0].objects)):
|
|
collide_colls[0].objects.unlink(collide_colls[0].objects[0])
|
|
|
|
for ob in add_obs:
|
|
collide_colls[0].objects.link(ob)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_limit_to_type(Operator):
|
|
bl_idname = "goophys.limit_to_type"
|
|
bl_label = "Limit Collision to Physics Type"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Sets the active bones/chains to only collide with objects in the matching type collection"
|
|
|
|
def execute(self, context):
|
|
coll, collide_colls, control_coll, bake_coll = ensure_collections()
|
|
|
|
pb = context.active_pose_bone
|
|
|
|
if pb.gp_has_cl_physics:
|
|
pb.gp_chain_limit_collision = not pb.gp_chain_limit_collision
|
|
|
|
elif pb.gp_has_gn_physics:
|
|
pb.gp_chain_limit_collision = not pb.gp_chain_limit_collision
|
|
|
|
elif pb.gp_has_sb_physics:
|
|
pb.gp_sim_limit_collision = not pb.gp_sim_limit_collision
|
|
|
|
elif pb.gp_has_jg_physics:
|
|
pb.gp_sim_limit_collision = not pb.gp_sim_limit_collision
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_add_bone_map_preset(Operator):
|
|
bl_idname = "goophys.add_bone_map_preset"
|
|
bl_label = "Add Bone Map Preset"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Adds a new Bone Map Preset from all physics bones settings"
|
|
|
|
name: bpy.props.StringProperty(
|
|
name="Preset Name",
|
|
description="Name of the preset",
|
|
default="",
|
|
)
|
|
|
|
description: bpy.props.StringProperty(
|
|
name="Preset Description",
|
|
description="Description of the preset",
|
|
default="",
|
|
)
|
|
|
|
def execute(self, context):
|
|
|
|
if self.name == "":
|
|
self.report(
|
|
{"ERROR"},
|
|
"You need to add a preset name to save the preset",
|
|
)
|
|
return {"CANCELLED"}
|
|
|
|
if context.active_object.type != "ARMATURE":
|
|
self.report(
|
|
{"ERROR"},
|
|
"Active object is not an armature",
|
|
)
|
|
return {"CANCELLED"}
|
|
|
|
p_dat = {}
|
|
p_dat["Name"] = self.name
|
|
p_dat["Description"] = self.description
|
|
p_dat["Chain Settings"] = []
|
|
|
|
user_preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets"
|
|
if os.path.exists(user_preset_fp) == False:
|
|
os.makedirs(bpy.path.abspath(str(user_preset_fp)))
|
|
|
|
for pb in context.active_object.pose.bones:
|
|
b_dat = {}
|
|
s_dat = {}
|
|
|
|
if pb.gp_has_cl_physics:
|
|
phys_type = "CLOTH"
|
|
|
|
s_dat["gp_chain_speed"] = pb.gp_chain_speed
|
|
|
|
s_dat["gp_chain_mass"] = pb.gp_chain_mass
|
|
|
|
s_dat["gp_chain_air_dampening"] = pb.gp_chain_air_dampening
|
|
s_dat["gp_chain_bend_damping"] = pb.gp_chain_bend_damping
|
|
s_dat["gp_chain_collision_dist"] = pb.gp_chain_collision_dist
|
|
s_dat["gp_chain_gravity"] = pb.gp_chain_gravity
|
|
|
|
s_dat["gp_sim_influence"] = pb.gp_sim_influence
|
|
|
|
elif pb.gp_has_sb_physics:
|
|
phys_type = "SOFT_BODY"
|
|
|
|
s_dat["gp_sim_speed"] = pb.gp_sim_speed
|
|
|
|
s_dat["gp_sim_friction"] = pb.gp_sim_friction
|
|
s_dat["gp_sim_mass"] = pb.gp_sim_mass
|
|
s_dat["gp_sim_gravity"] = pb.gp_sim_gravity
|
|
|
|
s_dat["gp_sim_stiffness"] = pb.gp_sim_stiffness
|
|
s_dat["gp_sim_damping"] = pb.gp_sim_damping
|
|
s_dat["gp_sim_strength"] = pb.gp_sim_strength
|
|
|
|
s_dat["gp_sim_influence"] = pb.gp_sim_influence
|
|
|
|
elif pb.gp_has_jg_physics:
|
|
phys_type = "JIGGLE"
|
|
|
|
s_dat["gp_sim_speed"] = pb.gp_sim_speed
|
|
|
|
s_dat["gp_sim_friction"] = pb.gp_sim_friction
|
|
s_dat["gp_sim_mass"] = pb.gp_sim_mass
|
|
s_dat["gp_sim_gravity"] = pb.gp_sim_gravity
|
|
|
|
s_dat["gp_sim_stiffness"] = pb.gp_sim_stiffness
|
|
s_dat["gp_sim_damping"] = pb.gp_sim_damping
|
|
|
|
s_dat["gp_sim_influence"] = pb.gp_sim_influence
|
|
|
|
elif pb.gp_has_gn_physics:
|
|
phys_type = "GEO_NODES"
|
|
|
|
s_dat["gp_chain_velocity"] = pb.gp_chain_velocity
|
|
s_dat["gp_chain_dampening"] = pb.gp_chain_dampening
|
|
s_dat["gp_chain_gravity"] = pb.gp_chain_gravity
|
|
s_dat["gp_chain_root_falloff"] = pb.gp_chain_root_falloff
|
|
|
|
s_dat["gp_chain_stiffness"] = pb.gp_chain_stiffness
|
|
s_dat["gp_chain_stiff_end_fac"] = pb.gp_chain_stiff_end_fac
|
|
s_dat["gp_chain_stiff_vel_fac"] = pb.gp_chain_stiff_vel_fac
|
|
s_dat["gp_chain_stiff_vel_min"] = pb.gp_chain_stiff_vel_min
|
|
s_dat["gp_chain_stiff_vel_max"] = pb.gp_chain_stiff_vel_max
|
|
|
|
s_dat["gp_chain_wind_strength"] = pb.gp_chain_wind_strength
|
|
s_dat["gp_chain_wind_noise_strength"] = pb.gp_chain_wind_noise_strength
|
|
s_dat["gp_chain_wind_noise_scale"] = pb.gp_chain_wind_noise_scale
|
|
|
|
s_dat["gp_chain_collision_dist"] = pb.gp_chain_collision_dist
|
|
s_dat["gp_chain_collision_friction"] = pb.gp_chain_collision_friction
|
|
|
|
s_dat["gp_sim_influence"] = pb.gp_sim_influence
|
|
|
|
else:
|
|
continue
|
|
|
|
b_dat["Bone Name"] = pb.name
|
|
b_dat["Settings"] = s_dat
|
|
b_dat["Chain ID"] = pb.gp_chain_id
|
|
b_dat["Chain Type"] = phys_type
|
|
p_dat["Chain Settings"].append(b_dat)
|
|
|
|
preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "bone_map_presets.json"
|
|
|
|
presets_data = {}
|
|
if os.path.exists(preset_fp):
|
|
presets_data = json.load(open(str(preset_fp)))
|
|
|
|
presets_data[self.name.replace(" ", "").upper()] = p_dat
|
|
|
|
preset_file = open(preset_fp, "w")
|
|
|
|
json.dump(presets_data, preset_file, indent=1, ensure_ascii=True)
|
|
|
|
preset_file.close()
|
|
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_props_dialog(self, width=800)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
row = layout.row()
|
|
row.prop(self, "name")
|
|
|
|
row = layout.row()
|
|
row.prop(self, "description")
|
|
|
|
|
|
class GP_OT_delete_bone_map_preset(Operator):
|
|
bl_idname = "goophys.delete_bone_map_preset"
|
|
bl_label = "Delete Bone Map Preset"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Deletes the active preset"
|
|
|
|
def execute(self, context):
|
|
prefs = context.preferences.addons["goo_physics"].preferences
|
|
|
|
preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "bone_map_presets.json"
|
|
presets_enum = prefs.bm_presets
|
|
|
|
presets_data = {}
|
|
if os.path.exists(preset_fp):
|
|
presets_data = json.load(open(str(preset_fp)))
|
|
|
|
if presets_enum.replace(" ", "").upper() in presets_data.keys():
|
|
del presets_data[presets_enum.replace(" ", "").upper()]
|
|
|
|
preset_file = open(preset_fp, "w")
|
|
|
|
json.dump(presets_data, preset_file, indent=1, ensure_ascii=True)
|
|
|
|
preset_file.close()
|
|
|
|
presets_enum = 0
|
|
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_props_dialog(self, width=400, confirm_text="Delete Preset? Cannot be recovered")
|
|
|
|
|
|
class GP_OT_apply_bone_map_preset(Operator):
|
|
bl_idname = "goophys.apply_bone_map_preset"
|
|
bl_label = "Apply Bone Map Preset"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Applys the active preset to the select bones/bone chains"
|
|
|
|
def execute(self, context):
|
|
|
|
if context.active_object.type != "ARMATURE":
|
|
self.report(
|
|
{"ERROR"},
|
|
"Active object is not an armature",
|
|
)
|
|
return {"CANCELLED"}
|
|
|
|
coll, collide_colls, control_coll, bake_coll = ensure_collections()
|
|
|
|
preset_data = get_bone_map_preset_data()
|
|
|
|
used_bones = []
|
|
for b_dat in preset_data:
|
|
used_bones.append(b_dat["Bone Name"])
|
|
|
|
phys_bones = []
|
|
for pb in context.active_object.pose.bones:
|
|
if pb.name not in used_bones:
|
|
continue
|
|
|
|
if pb.gp_has_cl_physics:
|
|
phys_bones.append(pb)
|
|
elif pb.gp_has_sb_physics:
|
|
phys_bones.append(pb)
|
|
elif pb.gp_has_jg_physics:
|
|
phys_bones.append(pb)
|
|
elif pb.gp_has_gn_physics:
|
|
phys_bones.append(pb)
|
|
|
|
sim_chains = get_partial_selected_bone_chains(phys_bones, include_all=True)
|
|
|
|
if len(sim_chains) > 0:
|
|
clear_sim_chains(sim_chains)
|
|
|
|
fail_bones = apply_bone_map_preset(preset_data, coll, collide_colls)
|
|
|
|
if len(fail_bones) > 0:
|
|
self.report(
|
|
{"ERROR"},
|
|
"These bones were missing from the active armature when applying the Bone Map Preset - " + str(fail_bones),
|
|
)
|
|
|
|
return {"FINISHED"}
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
layout.label(text="WARNING:")
|
|
layout.label(text="Applying a Bone Map Preset will remove all goo physics chains and objects used by the preset")
|
|
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_props_dialog(self, width=600, confirm_text="Apply Bone Map Preset?")
|
|
|
|
|
|
class GP_OT_add_preset(Operator):
|
|
bl_idname = "goophys.add_preset"
|
|
bl_label = "Add Preset"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Adds a new preset from the active bones settings"
|
|
|
|
name: bpy.props.StringProperty(
|
|
name="Preset Name",
|
|
description="Name of the preset",
|
|
default="",
|
|
)
|
|
|
|
description: bpy.props.StringProperty(
|
|
name="Preset Description",
|
|
description="Description of the preset",
|
|
default="",
|
|
)
|
|
|
|
chain_preset: bpy.props.BoolProperty(
|
|
name="Chain Preset",
|
|
description="This preset will save the value profile of the who chain for the bone settings",
|
|
default=True,
|
|
)
|
|
|
|
def execute(self, context):
|
|
|
|
if self.name == "":
|
|
self.report(
|
|
{"ERROR"},
|
|
"You need to add a preset name to save the preset",
|
|
)
|
|
return {"CANCELLED"}
|
|
|
|
resample_amount = 50
|
|
smooth_iters = 5
|
|
|
|
pb = context.active_pose_bone
|
|
p_dat = {}
|
|
|
|
if self.chain_preset and pb.gp_has_jg_physics == False:
|
|
p_dat["Name"] = self.name.replace("_CHAIN", "") + "_CHAIN"
|
|
else:
|
|
p_dat["Name"] = self.name
|
|
|
|
p_dat["Description"] = self.description
|
|
s_dat = {}
|
|
|
|
user_preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets"
|
|
if os.path.exists(user_preset_fp) == False:
|
|
os.makedirs(bpy.path.abspath(str(user_preset_fp)))
|
|
|
|
sim_chains = get_partial_selected_bone_chains(context.selected_pose_bones_from_active_object, only_active=True)
|
|
for chain in sim_chains:
|
|
if pb.gp_has_cl_physics:
|
|
|
|
s_dat["gp_chain_speed"] = pb.gp_chain_speed
|
|
|
|
s_dat["gp_chain_mass"] = pb.gp_chain_mass
|
|
s_dat["gp_sim_gravity"] = pb.gp_chain_gravity
|
|
|
|
s_dat["gp_chain_air_dampening"] = pb.gp_chain_air_dampening
|
|
s_dat["gp_chain_bend_damping"] = pb.gp_chain_bend_damping
|
|
s_dat["gp_chain_collision_dist"] = pb.gp_chain_collision_dist
|
|
|
|
if self.chain_preset:
|
|
s_dat["gp_sim_influence"] = []
|
|
|
|
for b_dat in chain:
|
|
bo = b_dat[0].pose.bones[b_dat[1]]
|
|
|
|
s_dat["gp_sim_influence"].append(bo.gp_sim_influence)
|
|
else:
|
|
s_dat["gp_sim_influence"] = pb.gp_sim_influence
|
|
|
|
preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "cloth_presets.json"
|
|
|
|
elif pb.gp_has_sb_physics:
|
|
|
|
if self.chain_preset:
|
|
s_dat["gp_sim_speed"] = []
|
|
|
|
s_dat["gp_sim_friction"] = []
|
|
s_dat["gp_sim_mass"] = []
|
|
s_dat["gp_sim_gravity"] = []
|
|
|
|
s_dat["gp_sim_stiffness"] = []
|
|
s_dat["gp_sim_damping"] = []
|
|
s_dat["gp_sim_strength"] = []
|
|
|
|
s_dat["gp_sim_influence"] = []
|
|
|
|
for b_dat in chain:
|
|
bo = b_dat[0].pose.bones[b_dat[1]]
|
|
|
|
s_dat["gp_sim_speed"].append(bo.gp_sim_speed)
|
|
|
|
s_dat["gp_sim_friction"].append(bo.gp_sim_friction)
|
|
s_dat["gp_sim_mass"].append(bo.gp_sim_mass)
|
|
s_dat["gp_sim_gravity"].append(bo.gp_sim_gravity)
|
|
|
|
s_dat["gp_sim_stiffness"].append(bo.gp_sim_stiffness)
|
|
s_dat["gp_sim_damping"].append(bo.gp_sim_damping)
|
|
s_dat["gp_sim_strength"].append(bo.gp_sim_strength)
|
|
|
|
s_dat["gp_sim_influence"].append(bo.gp_sim_influence)
|
|
else:
|
|
s_dat["gp_sim_speed"] = pb.gp_sim_speed
|
|
|
|
s_dat["gp_sim_friction"] = pb.gp_sim_friction
|
|
s_dat["gp_sim_mass"] = pb.gp_sim_mass
|
|
s_dat["gp_sim_gravity"] = pb.gp_sim_gravity
|
|
|
|
s_dat["gp_sim_stiffness"] = pb.gp_sim_stiffness
|
|
s_dat["gp_sim_damping"] = pb.gp_sim_damping
|
|
s_dat["gp_sim_strength"] = pb.gp_sim_strength
|
|
|
|
s_dat["gp_sim_influence"] = pb.gp_sim_influence
|
|
|
|
preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "soft_body_presets.json"
|
|
|
|
elif pb.gp_has_jg_physics:
|
|
|
|
s_dat["gp_sim_speed"] = pb.gp_sim_speed
|
|
|
|
s_dat["gp_sim_friction"] = pb.gp_sim_frictio
|
|
s_dat["gp_sim_mass"] = pb.gp_sim_mass
|
|
s_dat["gp_sim_gravity"] = pb.gp_sim_gravity
|
|
|
|
s_dat["gp_sim_stiffness"] = pb.gp_sim_stiffness
|
|
s_dat["gp_sim_damping"] = pb.gp_sim_damping
|
|
|
|
s_dat["gp_sim_influence"] = pb.gp_sim_influence
|
|
|
|
preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "jiggle_spring_presets.json"
|
|
|
|
elif pb.gp_has_gn_physics:
|
|
|
|
s_dat["gp_chain_velocity"] = pb.gp_chain_velocity
|
|
s_dat["gp_chain_dampening"] = pb.gp_chain_dampening
|
|
s_dat["gp_chain_gravity"] = pb.gp_chain_gravity
|
|
s_dat["gp_chain_root_falloff"] = pb.gp_chain_root_falloff
|
|
|
|
s_dat["gp_chain_stiffness"] = pb.gp_chain_stiffness
|
|
s_dat["gp_chain_stiff_end_fac"] = pb.gp_chain_stiff_end_fac
|
|
s_dat["gp_chain_stiff_vel_fac"] = pb.gp_chain_stiff_vel_fac
|
|
s_dat["gp_chain_stiff_vel_min"] = pb.gp_chain_stiff_vel_min
|
|
s_dat["gp_chain_stiff_vel_max"] = pb.gp_chain_stiff_vel_max
|
|
|
|
s_dat["gp_chain_wind_strength"] = pb.gp_chain_wind_strength
|
|
s_dat["gp_chain_wind_noise_strength"] = pb.gp_chain_wind_noise_strength
|
|
s_dat["gp_chain_wind_noise_scale"] = pb.gp_chain_wind_noise_scale
|
|
|
|
s_dat["gp_chain_collision_dist"] = pb.gp_chain_collision_dist
|
|
s_dat["gp_chain_collision_friction"] = pb.gp_chain_collision_friction
|
|
|
|
if self.chain_preset:
|
|
s_dat["gp_sim_influence"] = []
|
|
|
|
for b_dat in chain:
|
|
bo = b_dat[0].pose.bones[b_dat[1]]
|
|
|
|
s_dat["gp_sim_influence"].append(bo.gp_sim_influence)
|
|
else:
|
|
s_dat["gp_sim_influence"] = pb.gp_sim_influence
|
|
|
|
preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "geo_nodes_presets.json"
|
|
|
|
# Resample the values profile and smooth the inbetween values
|
|
for k in s_dat.keys():
|
|
if isinstance(s_dat[k], list) == False:
|
|
continue
|
|
|
|
vals = np.array(s_dat[k], dtype=np.float32)
|
|
|
|
if vals.shape[0] == 1:
|
|
|
|
s_dat[k] = [float(vals[0]) for i in range(resample_amount)]
|
|
|
|
else:
|
|
|
|
source_t_vals = np.arange(vals.shape[0], dtype=np.float32) / max([1, (vals.shape[0] - 1)])
|
|
|
|
t_vals = np.arange(resample_amount, dtype=np.float32) / (resample_amount - 1)
|
|
|
|
tar_inds = np.searchsorted(source_t_vals, t_vals)
|
|
|
|
p_inds = tar_inds - 1
|
|
tar_inds[0] = 1
|
|
p_inds[0] = 0
|
|
|
|
tar_inds[tar_inds < 0] = 0
|
|
tar_inds[tar_inds > source_t_vals.shape[0]] = 0
|
|
|
|
p_inds[p_inds < 0] = 0
|
|
p_inds[p_inds > source_t_vals.shape[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
|
|
|
|
inds = np.arange(lerp_vals.shape[0]-2, dtype=np.int32) + 1
|
|
next_inds = inds + 1
|
|
prev_inds = inds - 1
|
|
|
|
for i in range(smooth_iters):
|
|
neighbors = (lerp_vals[next_inds] + lerp_vals[prev_inds]) * 0.5
|
|
|
|
lerp_vals[inds] = lerp_vals[inds] * 0.5 + neighbors * 0.5
|
|
|
|
s_dat[k] = lerp_vals.tolist()
|
|
|
|
p_dat["Settings"] = s_dat
|
|
|
|
presets_data = {}
|
|
if os.path.exists(preset_fp):
|
|
presets_data = json.load(open(str(preset_fp)))
|
|
|
|
presets_data[self.name.replace(" ", "").upper()] = p_dat
|
|
|
|
preset_file = open(preset_fp, "w")
|
|
|
|
json.dump(presets_data, preset_file, indent=1, ensure_ascii=True)
|
|
|
|
preset_file.close()
|
|
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_props_dialog(self, width=800)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
row = layout.row()
|
|
row.prop(self, "name")
|
|
|
|
row = layout.row()
|
|
row.prop(self, "description")
|
|
|
|
if context.active_pose_bone.gp_has_jg_physics == False:
|
|
row = layout.row()
|
|
row.prop(self, "chain_preset")
|
|
|
|
|
|
class GP_OT_delete_preset(Operator):
|
|
bl_idname = "goophys.delete_preset"
|
|
bl_label = "Delete Preset"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Deletes the active preset"
|
|
|
|
def execute(self, context):
|
|
prefs = context.preferences.addons["goo_physics"].preferences
|
|
|
|
pb = context.active_pose_bone
|
|
if pb.gp_has_cl_physics:
|
|
preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "cloth_presets.json"
|
|
presets_enum = prefs.cl_presets
|
|
|
|
elif pb.gp_has_sb_physics:
|
|
preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "soft_body_presets.json"
|
|
presets_enum = prefs.sb_presets
|
|
|
|
elif pb.gp_has_jg_physics:
|
|
preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "jiggle_spring_presets.json"
|
|
presets_enum = prefs.jg_presets
|
|
|
|
elif pb.gp_has_gn_physics:
|
|
preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "geo_nodes_presets.json"
|
|
presets_enum = prefs.gn_presets
|
|
|
|
presets_data = {}
|
|
if os.path.exists(preset_fp):
|
|
presets_data = json.load(open(str(preset_fp)))
|
|
|
|
if presets_enum.replace(" ", "").upper() in presets_data.keys():
|
|
del presets_data[presets_enum.replace(" ", "").upper()]
|
|
|
|
preset_file = open(preset_fp, "w")
|
|
|
|
json.dump(presets_data, preset_file, indent=1, ensure_ascii=True)
|
|
|
|
preset_file.close()
|
|
|
|
presets_enum = 0
|
|
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
prefs = context.preferences.addons["goo_physics"].preferences
|
|
|
|
pb = context.active_pose_bone
|
|
if pb.gp_has_cl_physics:
|
|
preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "cloth_presets.json"
|
|
presets_enum = prefs.cl_presets
|
|
|
|
elif pb.gp_has_sb_physics:
|
|
preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "soft_body_presets.json"
|
|
presets_enum = prefs.sb_presets
|
|
|
|
elif pb.gp_has_jg_physics:
|
|
preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "jiggle_spring_presets.json"
|
|
presets_enum = prefs.jg_presets
|
|
|
|
elif pb.gp_has_gn_physics:
|
|
preset_fp = Path(os.path.dirname(__file__)) / "user_created_presets" / "geo_nodes_presets.json"
|
|
presets_enum = prefs.gn_presets
|
|
|
|
if presets_enum in ["DEFAULTSOFTBODY",
|
|
"DEFAULTGEONODES",
|
|
"DEFAULTJIGGLE",
|
|
"DEFAULTCLOTH",
|
|
"FLOPPYCLOTH",
|
|
"HAIRFRINGE",
|
|
"HAIRSIDE",
|
|
"HAIRPONYTAIL",
|
|
"JIGGLELOOSE",
|
|
"JIGGLESTIFF",
|
|
"SLOWMO",
|
|
"GOOFLAPPY",
|
|
"GOOSTIFF",
|
|
"METAL/JEWELRY",
|
|
"GOOSIM",
|
|
"STIFFCLOTH",
|
|
"FLOPPYCLOTH",]:
|
|
self.report(
|
|
{"ERROR"},
|
|
"Cannot delete the base presets",
|
|
)
|
|
return {"CANCELLED"}
|
|
|
|
return context.window_manager.invoke_props_dialog(self, width=400, confirm_text="Delete Preset? Cannot be recovered")
|
|
|
|
|
|
class GP_OT_apply_preset(Operator):
|
|
bl_idname = "goophys.apply_preset"
|
|
bl_label = "Apply Preset"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Applys the active preset to the select bones/bone chains"
|
|
|
|
def execute(self, context):
|
|
pb = context.active_pose_bone
|
|
|
|
preset_data = get_preset_data()
|
|
if preset_data is None:
|
|
self.report(
|
|
{"ERROR"},
|
|
"Active Pose Bone has no physics",
|
|
)
|
|
return {"CANCELLED"}
|
|
|
|
apply_preset(preset_data, pb)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_select_dynamic_bones(Operator):
|
|
bl_idname = "goophys.select_dynamic_bones"
|
|
bl_label = "Select Physics Bones"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Selects all bones with physics currently on them"
|
|
|
|
def execute(self, context):
|
|
for pb in context.visible_pose_bones:
|
|
if pb.gp_has_sb_physics or pb.gp_has_cl_physics or pb.gp_has_gn_physics or pb.gp_has_jg_physics:
|
|
set_bone_sel(pb, True)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_select_last_dynamic_bone(Operator):
|
|
bl_idname = "goophys.select_last_dynamic_bones"
|
|
bl_label = "Select Last In Chain"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Selects last bone in dynamic chain"
|
|
|
|
def execute(self, context):
|
|
for pb in context.visible_pose_bones:
|
|
if pb.gp_has_sb_physics or pb.gp_has_cl_physics or pb.gp_has_gn_physics or pb.gp_has_jg_physics:
|
|
|
|
not_last = False
|
|
for child in pb.children_recursive:
|
|
if child.gp_has_sb_physics or child.gp_has_cl_physics or child.gp_has_gn_physics or pb.gp_has_jg_physics:
|
|
not_last = True
|
|
|
|
set_bone_sel(pb, not not_last)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_add_geonodes_physics_to_selected(Operator):
|
|
bl_idname = "goophys.add_geonodes_physics_to_selected"
|
|
bl_label = "Add Physics to Selected Bones"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Adds Geo Nodes dynamic meshes to selected bone chains"
|
|
|
|
def execute(self, context):
|
|
prefs = context.preferences.addons["goo_physics"].preferences
|
|
|
|
coll, collide_colls, control_coll, bake_coll = ensure_collections()
|
|
|
|
sim_chains = get_bone_chains(context.selected_pose_bones)
|
|
|
|
if len(sim_chains) == 0:
|
|
self.report(
|
|
{"ERROR"},
|
|
"None of the bones you selected can be simmed. The bones need a parent to work",
|
|
)
|
|
|
|
# remove existing physics from selected bones
|
|
clear_sim_chains(sim_chains)
|
|
|
|
# cur_frame = context.scene.frame_current
|
|
context.scene.frame_set(context.scene.frame_start)
|
|
|
|
sim_geo_obs = create_geonodes_chains(
|
|
sim_chains, coll, collide_colls, prefs.gp_divisions
|
|
)
|
|
|
|
preset_data = get_preset_data()
|
|
if preset_data is not None:
|
|
apply_preset(preset_data, context.active_pose_bone)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_add_soft_physics_to_selected(Operator):
|
|
bl_idname = "goophys.add_soft_physics_to_selected"
|
|
bl_label = "Add Physics to Selected Bones"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Adds Soft Body dynamic meshes to selected bone chains"
|
|
|
|
def execute(self, context):
|
|
|
|
coll, collide_colls, control_coll, bake_coll = ensure_collections()
|
|
|
|
sim_chains = get_bone_chains(context.selected_pose_bones)
|
|
|
|
if len(sim_chains) == 0:
|
|
self.report(
|
|
{"ERROR"},
|
|
"None of the bones you selected can be simmed. The bones need a parent to work",
|
|
)
|
|
|
|
# remove existing physics from selected bones
|
|
clear_sim_chains(sim_chains)
|
|
|
|
sim_soft_obs = create_soft_chains(sim_chains, coll, collide_colls)
|
|
|
|
# cur_frame = context.scene.frame_current
|
|
context.scene.frame_set(context.scene.frame_start)
|
|
|
|
preset_data = get_preset_data()
|
|
if preset_data is not None:
|
|
apply_preset(preset_data, context.active_pose_bone)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_add_cloth_physics_to_selected(Operator):
|
|
bl_idname = "goophys.add_cloth_physics_to_selected"
|
|
bl_label = "Add Physics to Selected Bones"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Adds Soft Body dynamic meshes to selected bone chains"
|
|
|
|
def execute(self, context):
|
|
|
|
coll, collide_colls, control_coll, bake_coll = ensure_collections()
|
|
|
|
sim_chains = get_bone_chains(context.selected_pose_bones)
|
|
|
|
if len(sim_chains) == 0:
|
|
self.report(
|
|
{"ERROR"},
|
|
"None of the bones you selected can be simmed. The bones need a parent to work",
|
|
)
|
|
|
|
# remove existing physics from selected bones
|
|
clear_sim_chains(sim_chains)
|
|
|
|
sim_cloth_obs = create_cloth_chains(sim_chains, coll, collide_colls)
|
|
|
|
# cur_frame = context.scene.frame_current
|
|
context.scene.frame_set(context.scene.frame_start)
|
|
|
|
preset_data = get_preset_data()
|
|
if preset_data is not None:
|
|
apply_preset(preset_data, context.active_pose_bone)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_add_jiggle_physics_to_selected(Operator):
|
|
bl_idname = "goophys.add_jiggle_physics_to_selected"
|
|
bl_label = "Add Physics to Selected Bones"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Adds Soft Body dynamic meshes to selected bone chains"
|
|
|
|
def execute(self, context):
|
|
|
|
coll, collide_colls, control_coll, bake_coll = ensure_collections()
|
|
|
|
sim_chains = []
|
|
for bo in context.selected_pose_bones:
|
|
if bo.parent is not None and bo.bone.use_connect == False:
|
|
sim_chains.append([[bo.id_data, bo.name]])
|
|
|
|
if len(sim_chains) == 0:
|
|
self.report(
|
|
{"ERROR"},
|
|
"None of the bones you selected can be simmed. The bones need a Disconnected Parent (Keep Offset) to work",
|
|
)
|
|
|
|
# remove existing physics from selected bones
|
|
clear_sim_chains(sim_chains)
|
|
|
|
sim_spring_obs = create_spring_chains(sim_chains, coll, collide_colls)
|
|
|
|
# cur_frame = context.scene.frame_current
|
|
context.scene.frame_set(context.scene.frame_start)
|
|
|
|
preset_data = get_preset_data()
|
|
if preset_data is not None:
|
|
apply_preset(preset_data, context.active_pose_bone)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_remove_physics_from_selected(Operator):
|
|
bl_idname = "goophys.remove_physics_from_selected"
|
|
bl_label = "Remove Physics from Selected Bones"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Removes Soft Body dynamic meshes assigned to the selected bones"
|
|
|
|
def execute(self, context):
|
|
sim_chains = get_partial_selected_bone_chains(context.selected_pose_bones, include_all=True)
|
|
|
|
if len(sim_chains) == 0:
|
|
return {"FINISHED"}
|
|
|
|
clear_sim_chains(sim_chains)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_sync_frame_range(Operator):
|
|
bl_idname = "goophys.sync_frame_range"
|
|
bl_label = "Sync Frame Range"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Sync all soft body sim objects to the current frame range"
|
|
|
|
def execute(self, context):
|
|
|
|
coll, collide_colls, control_coll, bake_coll = ensure_collections()
|
|
|
|
if coll:
|
|
for ob in coll.objects:
|
|
mod = ob.modifiers.get("GP_SoftBody Sim")
|
|
if mod:
|
|
if context.scene.use_preview_range:
|
|
mod.point_cache.frame_start = context.scene.frame_preview_start
|
|
mod.point_cache.frame_end = context.scene.frame_preview_end
|
|
else:
|
|
mod.point_cache.frame_start = context.scene.frame_start
|
|
mod.point_cache.frame_end = context.scene.frame_end
|
|
|
|
mod = ob.modifiers.get("GP_Jiggle Sim")
|
|
if mod:
|
|
if context.scene.use_preview_range:
|
|
mod.point_cache.frame_start = context.scene.frame_preview_start
|
|
mod.point_cache.frame_end = context.scene.frame_preview_end
|
|
else:
|
|
mod.point_cache.frame_start = context.scene.frame_start
|
|
mod.point_cache.frame_end = context.scene.frame_end
|
|
|
|
mod = ob.modifiers.get("GP_Cloth Sim")
|
|
if mod:
|
|
if context.scene.use_preview_range:
|
|
mod.point_cache.frame_start = (
|
|
context.scene.frame_preview_start
|
|
)
|
|
mod.point_cache.frame_end = context.scene.frame_preview_end
|
|
else:
|
|
mod.point_cache.frame_start = context.scene.frame_start
|
|
mod.point_cache.frame_end = context.scene.frame_end
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_refresh_physics_status(Operator):
|
|
bl_idname = "goophys.refresh_physics_status"
|
|
bl_label = "Refresh Physics Status"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Sync all soft body sim objects to the current frame range"
|
|
|
|
def execute(self, context):
|
|
|
|
coll, collide_colls, control_coll, bake_coll = ensure_collections()
|
|
|
|
if coll:
|
|
skips = []
|
|
for ob in coll.objects:
|
|
if len(ob.gp_bone_coll.coll) == 0:
|
|
continue
|
|
|
|
mod = ob.modifiers.get("GP_SoftBody Sim")
|
|
has_sb = mod is not None
|
|
if has_sb:
|
|
mod.show_viewport = True
|
|
ob.location = ob.location
|
|
|
|
mod = ob.modifiers.get("GP_Jiggle Sim")
|
|
has_jp = mod is not None
|
|
if has_jp:
|
|
mod.show_viewport = True
|
|
ob.location = ob.location
|
|
|
|
mod = ob.modifiers.get("GP_Cloth Sim")
|
|
has_cl = mod is not None
|
|
if has_cl:
|
|
mod.show_viewport = True
|
|
ob.location = ob.location
|
|
|
|
mod = ob.modifiers.get("GP_Nodes Sim")
|
|
has_gn = mod is not None
|
|
if has_gn:
|
|
mod.show_viewport = True
|
|
ob.location = ob.location
|
|
|
|
for c_ob in ob.gp_bone_coll.coll:
|
|
if c_ob.rig_object not in bpy.data.objects:
|
|
continue
|
|
|
|
rig = bpy.data.objects[c_ob.rig_object]
|
|
|
|
if rig not in skips:
|
|
skips.append(rig)
|
|
|
|
if (
|
|
rig.animation_data is not None
|
|
and rig.animation_data.action is not None
|
|
):
|
|
clean_action_of_props(rig.animation_data.action)
|
|
|
|
if c_ob.rig_bone not in rig.pose.bones:
|
|
continue
|
|
|
|
bone = rig.pose.bones[c_ob.rig_bone]
|
|
|
|
bone.gp_has_sb_physics = has_sb
|
|
bone.gp_has_jg_physics = has_jp
|
|
bone.gp_has_cl_physics = has_cl
|
|
bone.gp_has_gn_physics = has_gn
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_apply_falloff_to_selected(Operator):
|
|
bl_idname = "goophys.apply_falloff_to_selected"
|
|
bl_label = "Apply Falloff to Selected"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Applies a linear or quadratic falloff to the sim influence of the selected dynamic chains. The tips of the chains will follow the simulation more and the roots will follow the animation more"
|
|
|
|
type: bpy.props.IntProperty()
|
|
|
|
def execute(self, context):
|
|
sim_chains = get_partial_selected_bone_chains(context.selected_pose_bones)
|
|
|
|
for chain in sim_chains:
|
|
for p, p_dat in enumerate(chain):
|
|
p_dat[0].pose.bones[p_dat[1]].gp_update = False
|
|
|
|
for chain in sim_chains:
|
|
tot_len = len(chain)
|
|
|
|
for p, p_dat in enumerate(chain):
|
|
val = (p + 1) / tot_len
|
|
|
|
if self.type == 0:
|
|
inf = val
|
|
|
|
elif self.type == 1:
|
|
inf = 1 - get_falloff(val)
|
|
|
|
pb = p_dat[0].pose.bones[p_dat[1]]
|
|
pb.gp_sim_influence = inf
|
|
track_cons = pb.constraints.get("GP_Track")
|
|
if track_cons:
|
|
track_cons.influence = inf
|
|
|
|
for chain in sim_chains:
|
|
for p, p_dat in enumerate(chain):
|
|
p_dat[0].pose.bones[p_dat[1]].gp_update = True
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_add_collider(Operator):
|
|
bl_idname = "goophys.add_collider"
|
|
bl_label = "Add Collider from Selected"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Creates a new collider object and adds to the list"
|
|
|
|
convex_hull: bpy.props.BoolProperty(name="Convex Hull Colliders", default=False, description="Converts each collider object to a convex hull mesh for simpler collision meshes")
|
|
|
|
def execute(self, context):
|
|
|
|
coll, collide_colls, control_coll, bake_coll = ensure_collections()
|
|
|
|
link_fp = get_nodes_fp()
|
|
|
|
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]
|
|
|
|
collider_obs = []
|
|
for ob in context.selected_objects:
|
|
if ob.type == "MESH":
|
|
collider_obs.append(ob)
|
|
|
|
sel_obs_name = "GP_Selected_Collision Objects"
|
|
|
|
collider_object = create_collider_object(
|
|
collider_obs, sel_obs_name, collide_colls[0], comb_ob_ng, convex_hull=self.convex_hull
|
|
)
|
|
collider_object.display_type = "WIRE"
|
|
collider_object.data.update()
|
|
|
|
collider_object.modifiers.new("Collision", "COLLISION")
|
|
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_props_dialog(self, width=400)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
row = layout.row()
|
|
row.prop(self, "convex_hull")
|
|
|
|
|
|
class GP_OT_add_wind_object(Operator):
|
|
bl_idname = "goophys.add_wind_objects"
|
|
bl_label = "Add Wind Controller to Chains"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Adds wind object to selected chains"
|
|
|
|
def execute(self, context):
|
|
|
|
coll, collide_colls, control_coll, bake_coll = ensure_collections()
|
|
|
|
wind_emp = bpy.data.objects.new("GP_Wind_Controller_(Scale_for_Power)", None)
|
|
wind_emp.empty_display_size = 1.25
|
|
wind_emp.empty_display_type = "SINGLE_ARROW"
|
|
control_coll.objects.link(wind_emp)
|
|
|
|
wind_loc = (0,0,5)
|
|
|
|
if context.active_pose_bone is not None:
|
|
wind_loc = context.active_pose_bone.id_data.matrix_world @ context.active_pose_bone.head
|
|
wind_loc[2] += 0.5
|
|
|
|
wind_emp.location = wind_loc
|
|
wind_emp.rotation_euler = (-1.5708,0,0)
|
|
wind_emp.select_set(True)
|
|
|
|
for pb in context.selected_pose_bones:
|
|
if pb.gp_has_gn_physics:
|
|
ob = bpy.data.objects.get(pb.gp_sim_object)
|
|
if ob:
|
|
mod = ob.modifiers.get("GP_Nodes Sim")
|
|
if mod:
|
|
set_geo_nodes_input(mod, "Wind Object", wind_emp)
|
|
|
|
ob.location = ob.location
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_remove_wind_object(Operator):
|
|
bl_idname = "goophys.remove_wind_objects"
|
|
bl_label = "Remove Wind Controller from Chains"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Removes the set wind object from selected chains"
|
|
|
|
def execute(self, context):
|
|
coll = bpy.data.collections.get("Goo Physics Controllers")
|
|
if coll is not None:
|
|
for pb in context.selected_pose_bones:
|
|
if pb.gp_has_gn_physics:
|
|
ob = bpy.data.objects.get(pb.gp_sim_object)
|
|
if ob:
|
|
mod = ob.modifiers.get("GP_Nodes Sim")
|
|
if mod:
|
|
set_geo_nodes_input(mod, "Wind Object", None)
|
|
|
|
ob.location = ob.location
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_copy_active_wind_controller(Operator):
|
|
bl_idname = "goophys.copy_active_wind_controller"
|
|
bl_label = "Copy Wind Controller to Selected Chains"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Adds the active chains wind object to the selected chains"
|
|
|
|
def execute(self, context):
|
|
coll = bpy.data.collections.get("Goo Physics Controllers")
|
|
if coll is not None:
|
|
wind_emp = None
|
|
if context.active_pose_bone.gp_has_gn_physics:
|
|
ob = bpy.data.objects.get(context.active_pose_bone.gp_sim_object)
|
|
if ob:
|
|
mod = ob.modifiers.get("GP_Nodes Sim")
|
|
if mod:
|
|
wind_emp = get_geo_nodes_input(mod, "Wind Object")
|
|
|
|
ob.location = ob.location
|
|
|
|
if wind_emp is not None:
|
|
for pb in context.selected_pose_bones:
|
|
if pb.gp_has_gn_physics:
|
|
ob = bpy.data.objects.get(pb.gp_sim_object)
|
|
if ob:
|
|
mod = ob.modifiers.get("GP_Nodes Sim")
|
|
if mod:
|
|
set_geo_nodes_input(mod, "Wind Object", wind_emp)
|
|
|
|
ob.location = ob.location
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class GP_OT_bake_physics(Operator):
|
|
bl_idname = "goophys.bake_physics"
|
|
bl_label = "Bake Bone Physics"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = (
|
|
"Bake the bone physics to keyframes on the current armature action"
|
|
)
|
|
|
|
use_magic: bpy.props.BoolProperty(name="Use Magic Empties", default=False, description="Bake the physics of the bones using Magic Empties settings")
|
|
sep_action: bpy.props.BoolProperty(name="Bake to New Action", default=False, description="Bake the physics to keyframes in a new action instead of overwriting the existing action")
|
|
|
|
def modal(self, context, event):
|
|
prefs = context.preferences.addons[__package__.split(".")[0]].preferences
|
|
context.area.tag_redraw()
|
|
|
|
frame_start = context.scene.frame_start
|
|
frame_end = context.scene.frame_end
|
|
if context.scene.use_preview_range:
|
|
frame_start = context.scene.frame_preview_start
|
|
frame_end = context.scene.frame_preview_end
|
|
|
|
if context.scene.frame_current == frame_start and self.is_going:
|
|
self.bake_physics(context)
|
|
return {"FINISHED"}
|
|
|
|
elif context.scene.frame_current > frame_start:
|
|
self.is_going = True
|
|
|
|
return {"RUNNING_MODAL"}
|
|
|
|
def execute(self, context):
|
|
|
|
self.is_going = False
|
|
if context.scene.sync_mode != "NONE":
|
|
prefs = context.preferences.addons["goo_physics"].preferences
|
|
|
|
frames = prefs.timeline_sync_frames.split(",")
|
|
|
|
if len(frames) < 5:
|
|
|
|
context.scene.frame_set(context.scene.frame_start)
|
|
|
|
context.window_manager.modal_handler_add(self)
|
|
|
|
prefs.precaching = True
|
|
bpy.ops.screen.animation_play()
|
|
return {"RUNNING_MODAL"}
|
|
|
|
self.bake_physics(context)
|
|
return {"FINISHED"}
|
|
|
|
def bake_physics(self, context):
|
|
prefs = context.preferences.addons["goo_physics"].preferences
|
|
|
|
coll, collide_colls, control_coll, bake_coll = ensure_collections()
|
|
coll.hide_viewport = False
|
|
for c in collide_colls:
|
|
c.hide_viewport = False
|
|
control_coll.hide_viewport = False
|
|
|
|
chain_ids, rigs = [], []
|
|
for bo in context.selected_pose_bones:
|
|
if (bo.gp_has_sb_physics or bo.gp_has_cl_physics or bo.gp_has_gn_physics) and bo.gp_chain_id not in chain_ids:
|
|
|
|
if [bo.id_data, bo.gp_chain_id] not in chain_ids:
|
|
chain_ids.append([bo.id_data, bo.gp_chain_id])
|
|
|
|
if bo.id_data not in rigs:
|
|
rigs.append(bo.id_data)
|
|
|
|
sim_chains = get_partial_selected_bone_chains(context.selected_pose_bones)
|
|
|
|
if len(sim_chains) == 0:
|
|
self.report(
|
|
{"ERROR"},
|
|
"Could not find any bone chains to bake. Try using Refresh Physics Status",
|
|
)
|
|
|
|
frame_start = context.scene.frame_start
|
|
frame_end = context.scene.frame_end
|
|
if context.scene.use_preview_range:
|
|
frame_start = context.scene.frame_preview_start
|
|
frame_end = context.scene.frame_preview_end
|
|
|
|
skip_rigs = []
|
|
for c, chain in enumerate(sim_chains):
|
|
|
|
for b, bo_dat in enumerate(chain):
|
|
rig = bo_dat[0]
|
|
bo = rig.pose.bones[bo_dat[1]]
|
|
|
|
if b == 0 and rig not in skip_rigs:
|
|
# Unselect bones of rigs so they dont all bake only physics bones bake
|
|
skip_rigs.append(rig)
|
|
|
|
chain_vis = []
|
|
rig.hide_viewport = False
|
|
rig.hide_select = False
|
|
for pb in rig.pose.bones:
|
|
set_bone_sel(pb, False)
|
|
|
|
rig.select_set(True)
|
|
context.view_layer.objects.active = rig
|
|
|
|
# Store bone layer/collection visibilities to restore after bake
|
|
coll_vis = []
|
|
if bpy.app.version[0] >= 4:
|
|
for bo_coll in rig.data.collections:
|
|
coll_vis.append(bo_coll.is_visible)
|
|
bo_coll.is_visible = True
|
|
else:
|
|
for l, layer in enumerate(rig.data.layers):
|
|
coll_vis.append(rig.data.layers[l])
|
|
rig.data.layers[l] = True
|
|
|
|
chain_vis.append([rig, coll_vis.copy()])
|
|
|
|
# Set physics object to right frame range for softbody and cloth
|
|
phys_ob = bpy.data.objects.get(bo.gp_sim_object)
|
|
if phys_ob:
|
|
mod = phys_ob.modifiers.get("GP_SoftBody Sim")
|
|
if mod:
|
|
mod.point_cache.frame_start = frame_start
|
|
mod.point_cache.frame_end = frame_end
|
|
|
|
mod = phys_ob.modifiers.get("GP_Jiggle Sim")
|
|
if mod:
|
|
mod.point_cache.frame_start = frame_start
|
|
mod.point_cache.frame_end = frame_end
|
|
|
|
mod = phys_ob.modifiers.get("GP_Cloth Sim")
|
|
if mod:
|
|
mod.point_cache.frame_start = frame_start
|
|
mod.point_cache.frame_end = frame_end
|
|
|
|
phys_ob.location = phys_ob.location
|
|
|
|
set_bone_sel(bo, True)
|
|
|
|
#
|
|
# BAKING
|
|
#
|
|
|
|
frames = None
|
|
if context.scene.sync_mode != "NONE":
|
|
fs = sorted([int(i) for i in prefs.timeline_sync_frames.split(",") if i.isdigit()])
|
|
|
|
frames = []
|
|
for f in fs:
|
|
if f not in frames:
|
|
frames.append(f)
|
|
|
|
if frame_start not in frames:
|
|
frames.insert(0, frame_start)
|
|
|
|
if frame_end not in frames:
|
|
frames.append(frame_end)
|
|
for f in frames:
|
|
context.scene.timeline_markers.new("GOO_BAKE_MARKER_" + str(f), frame=f)
|
|
|
|
need_reg_bake = True
|
|
if (is_addon_enabled("magic_empties", (1, 0, 0))) and self.use_magic:
|
|
|
|
try:
|
|
bpy.ops.magicemps.bake_to_empties(
|
|
use_armature=False,
|
|
uses_button=False,
|
|
use_marked_frames=context.scene.sync_mode != "NONE",
|
|
marker_filter="GOO_BAKE_MARKER_",
|
|
)
|
|
need_reg_bake = False
|
|
except:
|
|
need_reg_bake = True
|
|
|
|
if need_reg_bake:
|
|
bpy.ops.object.mode_set(mode="POSE")
|
|
b_types = {"POSE"}
|
|
|
|
for ob in context.selected_objects:
|
|
if ob.type != "ARMATURE":
|
|
ob.select_set(False)
|
|
|
|
try:
|
|
bpy.ops.nla.bake(
|
|
frame_start=frame_start,
|
|
frame_end=frame_end,
|
|
step=1,
|
|
clear_parents=False,
|
|
clear_constraints=True,
|
|
use_current_action=not self.sep_action,
|
|
only_selected=True,
|
|
visual_keying=True,
|
|
clean_curves=True,
|
|
bake_types=b_types,
|
|
use_marked_frames=context.scene.sync_mode != "NONE",
|
|
marker_filter="GOO_BAKE_MARKER_",
|
|
)
|
|
except:
|
|
bpy.ops.nla.bake(
|
|
frame_start=frame_start,
|
|
frame_end=frame_end,
|
|
step=1,
|
|
clear_parents=False,
|
|
clear_constraints=True,
|
|
use_current_action=not self.sep_action,
|
|
only_selected=True,
|
|
visual_keying=True,
|
|
bake_types=b_types,
|
|
use_marked_frames=context.scene.sync_mode != "NONE",
|
|
marker_filter="GOO_BAKE_MARKER_",
|
|
)
|
|
|
|
#
|
|
#
|
|
#
|
|
|
|
bpy.ops.object.mode_set(mode="POSE")
|
|
|
|
clear_sim_chains(sim_chains)
|
|
|
|
for c, c_dat in enumerate(chain_vis):
|
|
rig = c_dat[0]
|
|
if bpy.app.version[0] >= 4:
|
|
for b, bo_coll in enumerate(rig.data.collections):
|
|
bo_coll.is_visible = c_dat[1][b]
|
|
else:
|
|
for l, layer in enumerate(rig.data.layers):
|
|
rig.data.layers[l] = c_dat[1][l]
|
|
|
|
for act in bpy.data.actions:
|
|
clean_action_of_props(act)
|
|
|
|
for marker in [m for m in context.scene.timeline_markers if "GOO_BAKE_MARKER_" in m.name]:
|
|
context.scene.timeline_markers.remove(marker)
|
|
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
prefs = context.preferences.addons[__package__.split(".")[0]].preferences
|
|
prefs.precaching = False
|
|
return context.window_manager.invoke_props_dialog(self, width=800)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
if is_addon_enabled("magic_empties", (1, 0, 0)):
|
|
row = layout.row()
|
|
row.prop(self, "use_magic")
|
|
|
|
row = layout.row()
|
|
row.prop(self, "sep_action")
|
|
|
|
|
|
class GP_OT_colliders_from_mesh(Operator):
|
|
bl_idname = "goophys.colliders_from_mesh"
|
|
bl_label = "Add Collider from Selected"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
bl_description = "Creates new collider objects from the deform vertex groups of the selected mesh objects"
|
|
|
|
def execute(self, context):
|
|
|
|
coll, collide_colls, control_coll, bake_coll = ensure_collections()
|
|
|
|
link_fp = get_nodes_fp()
|
|
|
|
vg_create_ng = bpy.data.node_groups.get("GooPhysics_ColliderVgroupCreate")
|
|
if vg_create_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_ColliderVgroupCreate"]
|
|
]
|
|
|
|
vg_create_ng = data_to.node_groups[0]
|
|
|
|
colliders = []
|
|
for ob in context.selected_objects:
|
|
if ob.type != "MESH":
|
|
continue
|
|
|
|
arm_obs = []
|
|
for mod in ob.modifiers:
|
|
if mod.type == "ARMATURE":
|
|
if mod.object is not None:
|
|
arm_obs.append(mod.object)
|
|
|
|
for vg in ob.vertex_groups:
|
|
|
|
is_def = False
|
|
for arm_ob in arm_obs:
|
|
pb = arm_ob.pose.bones.get(vg.name)
|
|
|
|
if pb is None:
|
|
continue
|
|
|
|
is_def = pb.bone.use_deform
|
|
|
|
if is_def == False:
|
|
continue
|
|
|
|
collide_dat = bpy.data.meshes.new(pb.name + "_Collider")
|
|
collide_ob = bpy.data.objects.new(pb.name + "_Collider", collide_dat)
|
|
|
|
colliders.append(collide_ob)
|
|
|
|
mod = collide_ob.modifiers.new("Create", "NODES")
|
|
mod.node_group = vg_create_ng
|
|
|
|
set_geo_nodes_input(mod, "Target", ob)
|
|
set_geo_nodes_input(mod, "Vgroup", vg.name)
|
|
|
|
collide_ob.parent = pb.id_data
|
|
collide_ob.parent_type = "BONE"
|
|
collide_ob.parent_bone = pb.name
|
|
|
|
collide_ob.matrix_parent_inverse[1][3] = pb.bone.length * -1
|
|
|
|
collide_ob.location = [0.0, 0.0, 0.0]
|
|
collide_ob.scale = [1.0, 1.0, 1.0]
|
|
collide_ob.rotation_euler = [0.0, 0.0, 0.0]
|
|
|
|
collide_colls[0].objects.link(collide_ob)
|
|
|
|
context.scene.frame_set(context.scene.frame_current)
|
|
del_obs = []
|
|
for ob in colliders:
|
|
|
|
bm = bmesh.new()
|
|
depsg = context.evaluated_depsgraph_get()
|
|
ob_eval = ob.evaluated_get(depsg)
|
|
|
|
mesh_eval = bpy.data.meshes.new_from_object(ob_eval)
|
|
|
|
bm = bmesh.new()
|
|
bm.from_mesh(mesh_eval)
|
|
ob.data.clear_geometry()
|
|
bm.to_mesh(ob.data)
|
|
|
|
bm.free()
|
|
bpy.data.meshes.remove(mesh_eval)
|
|
|
|
ob.modifiers.clear()
|
|
|
|
if len(ob.data.vertices) < 8:
|
|
del_obs.append(ob)
|
|
|
|
ob.modifiers.new("Collision", "COLLISION")
|
|
ob.hide_render = True
|
|
|
|
for ob in del_obs:
|
|
bpy.data.meshes.remove(ob.data)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class NLA_OT_smart_bake(Operator):
|
|
"""Bake all selected objects loc/scale/rotation animation to an action"""
|
|
|
|
bl_idname = "nla.bake"
|
|
bl_label = "Bake Action"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
frame_start: IntProperty(
|
|
name="Start Frame",
|
|
description="Start frame for baking",
|
|
min=0,
|
|
max=300000,
|
|
default=1,
|
|
)
|
|
frame_end: IntProperty(
|
|
name="End Frame",
|
|
description="End frame for baking",
|
|
min=1,
|
|
max=300000,
|
|
default=250,
|
|
)
|
|
step: IntProperty(
|
|
name="Frame Step",
|
|
description="Frame Step",
|
|
min=1,
|
|
max=120,
|
|
default=1,
|
|
)
|
|
only_selected: BoolProperty(
|
|
name="Only Selected Bones",
|
|
description="Only key selected bones (Pose baking only)",
|
|
default=True,
|
|
)
|
|
visual_keying: BoolProperty(
|
|
name="Visual Keying",
|
|
description="Keyframe from the final transformations (with constraints applied)",
|
|
default=False,
|
|
)
|
|
clear_constraints: BoolProperty(
|
|
name="Clear Constraints",
|
|
description="Remove all constraints from keyed object/bones, and do 'visual' keying",
|
|
default=False,
|
|
)
|
|
clear_parents: BoolProperty(
|
|
name="Clear Parents",
|
|
description="Bake animation onto the object then clear parents (objects only)",
|
|
default=False,
|
|
)
|
|
use_current_action: BoolProperty(
|
|
name="Overwrite Current Action",
|
|
description="Bake animation into current action, instead of creating a new one "
|
|
"(useful for baking only part of bones in an armature)",
|
|
default=False,
|
|
)
|
|
clean_curves: BoolProperty(
|
|
name="Clean Curves",
|
|
description="After baking curves, remove redundant keys",
|
|
default=False,
|
|
)
|
|
bake_types: EnumProperty(
|
|
name="Bake Data",
|
|
description="Which data's transformations to bake",
|
|
options={"ENUM_FLAG"},
|
|
items=(
|
|
("POSE", "Pose", "Bake bones transformations"),
|
|
("OBJECT", "Object", "Bake object transformations"),
|
|
),
|
|
default={"POSE"},
|
|
)
|
|
do_smart_bake: BoolProperty(
|
|
name="Use Smart Bake",
|
|
description="Bake pose animation only on existing keyframes",
|
|
default=False,
|
|
)
|
|
use_marked_frames: BoolProperty(
|
|
name="Use Frames With Markers",
|
|
description="Only bake frames with markers",
|
|
default=False,
|
|
)
|
|
marker_filter: StringProperty(
|
|
name="Marker Filter",
|
|
description="Only use markers with the set filter in the name",
|
|
default="",
|
|
)
|
|
|
|
def execute(self, context):
|
|
from bpy_extras import anim_utils
|
|
|
|
do_pose = "POSE" in self.bake_types
|
|
do_object = "OBJECT" in self.bake_types
|
|
|
|
objects = context.selected_editable_objects
|
|
|
|
# If we have Smart Bake enabled, remove all pose objects and smart bake them separately
|
|
if self.do_smart_bake and "POSE" in self.bake_types:
|
|
non_pose_obs = []
|
|
for obj in objects:
|
|
if obj.type == "ARMATURE":
|
|
smart_bake_pose(
|
|
obj,
|
|
use_existing=self.use_current_action,
|
|
frame_start=self.frame_start,
|
|
frame_end=self.frame_end,
|
|
only_selected=self.only_selected,
|
|
do_visual_keying=self.visual_keying,
|
|
do_constraint_clear=self.clear_constraints,
|
|
do_parents_clear=self.clear_parents,
|
|
do_clean=True,
|
|
do_pose=True,
|
|
do_object=False,
|
|
)
|
|
else:
|
|
non_pose_obs.append(obj)
|
|
objects = non_pose_obs
|
|
|
|
# If we have Smart Bake enabled, remove all object objects and smart bake them separately
|
|
if self.do_smart_bake and "OBJECT" in self.bake_types:
|
|
for obj in objects:
|
|
smart_bake_pose(
|
|
obj,
|
|
use_existing=self.use_current_action,
|
|
frame_start=self.frame_start,
|
|
frame_end=self.frame_end,
|
|
only_selected=self.only_selected,
|
|
do_visual_keying=self.visual_keying,
|
|
do_constraint_clear=self.clear_constraints,
|
|
do_parents_clear=self.clear_parents,
|
|
do_clean=True,
|
|
do_pose=False,
|
|
do_object=True,
|
|
)
|
|
return {"FINISHED"}
|
|
|
|
if not len(objects):
|
|
return {"FINISHED"}
|
|
|
|
object_action_pairs = (
|
|
[(obj, getattr(obj.animation_data, "action", None)) for obj in objects]
|
|
if self.use_current_action
|
|
else [(obj, None) for obj in objects]
|
|
)
|
|
|
|
bake_frames = range(self.frame_start, self.frame_end + 1, self.step)
|
|
|
|
if self.use_marked_frames:
|
|
bake_frames = []
|
|
for m in context.scene.timeline_markers:
|
|
if self.marker_filter in m.name:
|
|
bake_frames.append(m.frame)
|
|
|
|
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=self.only_selected,
|
|
do_pose=do_pose,
|
|
do_object=do_object,
|
|
do_visual_keying=self.visual_keying,
|
|
do_constraint_clear=self.clear_constraints,
|
|
do_parents_clear=self.clear_parents,
|
|
do_clean=self.clean_curves,
|
|
do_location=True,
|
|
do_rotation=True,
|
|
do_scale=True,
|
|
do_bbone=False,
|
|
do_custom_props=False,
|
|
)
|
|
|
|
actions = anim_utils.bake_action_objects(
|
|
object_action_pairs,
|
|
frames=bake_frames,
|
|
bake_options=bake_opts,
|
|
)
|
|
else:
|
|
|
|
actions = anim_utils.bake_action_objects(
|
|
object_action_pairs,
|
|
frames=bake_frames,
|
|
only_selected=self.only_selected,
|
|
do_pose=do_pose,
|
|
do_object=do_object,
|
|
do_visual_keying=self.visual_keying,
|
|
do_constraint_clear=self.clear_constraints,
|
|
do_parents_clear=self.clear_parents,
|
|
do_clean=self.clean_curves,
|
|
)
|
|
|
|
if not any(actions):
|
|
self.report({"INFO"}, "Nothing to bake")
|
|
return {"CANCELLED"}
|
|
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, _event):
|
|
scene = context.scene
|
|
self.frame_start = scene.frame_start
|
|
self.frame_end = scene.frame_end
|
|
self.bake_types = {"POSE"} if context.mode == "POSE" else {"OBJECT"}
|
|
|
|
wm = context.window_manager
|
|
return wm.invoke_props_dialog(self)
|
|
|
|
|
|
#
|
|
#
|
|
#
|
|
|
|
|
|
_classes = [
|
|
GP_OT_refresh_collision_collection,
|
|
GP_OT_limit_to_type,
|
|
GP_OT_add_preset,
|
|
GP_OT_delete_preset,
|
|
GP_OT_apply_preset,
|
|
GP_OT_select_dynamic_bones,
|
|
GP_OT_select_last_dynamic_bone,
|
|
GP_OT_add_geonodes_physics_to_selected,
|
|
GP_OT_add_soft_physics_to_selected,
|
|
GP_OT_add_cloth_physics_to_selected,
|
|
GP_OT_add_jiggle_physics_to_selected,
|
|
GP_OT_sync_frame_range,
|
|
GP_OT_refresh_physics_status,
|
|
GP_OT_remove_physics_from_selected,
|
|
GP_OT_apply_falloff_to_selected,
|
|
GP_OT_add_collider,
|
|
GP_OT_add_wind_object,
|
|
GP_OT_remove_wind_object,
|
|
GP_OT_copy_active_wind_controller,
|
|
GP_OT_bake_physics,
|
|
GP_OT_colliders_from_mesh,
|
|
GP_OT_add_bone_map_preset,
|
|
GP_OT_delete_bone_map_preset,
|
|
GP_OT_apply_bone_map_preset,
|
|
]
|
|
|
|
|
|
_register, _unregister = register_classes_factory(_classes)
|
|
|
|
|
|
def register():
|
|
if is_addon_enabled("magic_empties", (1, 0, 0)) == False:
|
|
bpy.utils.register_class(NLA_OT_smart_bake)
|
|
_register()
|
|
|
|
|
|
def unregister():
|
|
_unregister()
|