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