├── .gitattributes ├── FishSim.py ├── README.md ├── __init__.py ├── blender_manifest.toml ├── images ├── FSim_SimPanel1.png ├── FSim_ToolPanel.png └── FSim_add_menu.png ├── metarig_menu.py ├── metarigs └── FishSim │ └── goldfish.py └── presets ├── GreatWhite.py └── goldfish.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.c text 7 | *.h text 8 | 9 | # Declare files that will always have CRLF line endings on checkout. 10 | *.sln text eol=crlf 11 | 12 | # Denote all files that are truly binary and should not be modified. 13 | *.png binary 14 | *.jpg binary 15 | 16 | -------------------------------------------------------------------------------- /FishSim.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # FishSim -- a script to apply a fish swimming simulation to an armature 4 | # by Ian Huish (nerk) 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU General Public License 8 | # as published by the Free Software Foundation; either version 2 9 | # of the License, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software Foundation, 18 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | # 20 | # ##### END GPL LICENSE BLOCK ##### 21 | 22 | # version comment: V4.02.0 - Goldfish Version - Blender 4.20 Extensions 23 | 24 | import bpy 25 | import mathutils, math, os 26 | from bpy.props import FloatProperty, FloatVectorProperty, IntProperty, BoolProperty, EnumProperty, StringProperty 27 | from random import random 28 | 29 | 30 | 31 | 32 | 33 | class FSimProps(bpy.types.PropertyGroup): 34 | 35 | #State Variables 36 | sVelocity : FloatVectorProperty(name="Velocity", description="Speed", subtype='XYZ', default=(0.0,0.0,0.0), min=-5.0, max=5.0) 37 | sEffort : FloatProperty(name="Effort", description="The effort going into swimming", default=1.0, min=0) 38 | sTurn : FloatProperty(name="Turn", description="The intent to go left of right (positive is right)", default=0.0) 39 | sRise : FloatProperty(name="Rise", description="The intent to go up or down (positive is up", default=0.0) 40 | sFreq : FloatProperty(name="Frequency", description="Current frequency of tail movement in frames per cycle", default=0.0) 41 | sTailAngle : FloatProperty(name="Tail Angle", description="Current max tail angle in degrees", default=0.0) 42 | sTailAngleOffset : FloatProperty(name="Tail Angle Offset", description="Offset angle for turning in degrees", default=0.0) 43 | 44 | #Property declaration 45 | pMass : FloatProperty(name="Mass", description="Total Mass", default=30.0, min=0, max=3000.0) 46 | pDrag : FloatProperty(name="Drag", description="Total Drag", default=8.0, min=0, max=3000.0) 47 | pPower : FloatProperty(name="Power", description="Forward force for given tail fin speed and angle", default=1.0, min=0) 48 | pMaxFreq : FloatProperty(name="Stroke Period", description="Maximum frequency of tail movement in frames per cycle", default=15.0) 49 | pEffortGain : FloatProperty(name="Effort Gain", description="The amount of effort required for a change in distance to target", default=0.5, min=0.0) 50 | pEffortIntegral : FloatProperty(name="Effort Integral", description="The amount of effort required for a continuing distance to target", default=0.5, min=0.0) 51 | pEffortRamp : FloatProperty(name="Effort Ramp", description="First Order factor for ramping up effort", default=0.2, min=0.0, max=0.6) 52 | pAngularDrag : FloatProperty(name="AngularDrag", description="Resistance to changing direction", default=1.0, min=0) 53 | pTurnAssist : FloatProperty(name="TurnAssist", description="Fake Turning effect (0 - 10)", default=3.0, min=0) 54 | pMaxTailAngle : FloatProperty(name="Max Tail Angle", description="Max tail angle", default=15.0, min=0, max=30.0) 55 | pMaxSteeringAngle : FloatProperty(name="Max Steering Angle", description="Max steering tail angle", default=15.0, min=0, max=40.0) 56 | pMaxVerticalAngle : FloatProperty(name="Max Vertical Angle", description="Max steering angle for vertical", default=0.1, min=0, max=40.0) 57 | pMaxTailFinAngle : FloatProperty(name="Max Tail Fin Angle", description="Max tail fin angle", default=15.0, min=0, max=30.0) 58 | pTailFinPhase : FloatProperty(name="Tail Fin Phase", description="Tail Fin phase offset from tail movement in degrees", default=90.0, min=45.0, max=135.0) 59 | pTailFinStiffness : FloatProperty(name="Tail Fin Stiffness", description="Tail Fin Stiffness", default=1.0, min=0, max=2.0) 60 | pTailFinStubRatio : FloatProperty(name="Tail Fin Stub Ratio", description="Ratio for the bottom part of the tail", default=0.3, min=0, max=3.0) 61 | pMaxSideFinAngle : FloatProperty(name="Max Side Fin Angle", description="Max side fin angle", default=5.0, min=0, max=60.0) 62 | pSideFinPhase : FloatProperty(name="Side Fin Phase", description="Side Fin phase offset from tail movement in degrees", default=90.0, min=45.0, max=135.0) 63 | # pSideFinStiffness : FloatProperty(name="Side Fin Stiffness", description="Side Fin Stiffness", default=0.2, min=0, max=10.0) 64 | pChestRatio : FloatProperty(name="Chest Ratio", description="Ratio of the front of the fish to the rear", default=0.5, min=0, max=2.0) 65 | pChestRaise : FloatProperty(name="Chest Raise Factor", description="Chest raises during turning", default=1.0, min=0, max=20.0) 66 | pLeanIntoTurn : FloatProperty(name="LeanIntoTurn", description="Amount it leans into the turns", default=1.0, min=0, max=20.0) 67 | pRandom : FloatProperty(name="Random", description="Random amount", default=0.25, min=0, max=1.0) 68 | 69 | #Pectoral Fin Properties 70 | pPecEffortGain : FloatProperty(name="Pectoral Effort Gain", description="Amount of effort to maintain position with 1.0 trying very hard to maintain", default=0.25, min=0, max=1.0) 71 | pPecTurnAssist : FloatProperty(name="Pectoral Turn Assist", description="Turning Speed while hovering 5 is fast, .2 is slow", default=1.0, min=0, max=20.0) 72 | 73 | pMaxPecFreq : FloatProperty(name="Pectoral Stroke Period", description="Maximum frequency of pectoral fin movement in frames per cycle", default=15.0, min=0) 74 | pMaxPecAngle : FloatProperty(name="Max Pec Fin Angle", description="Max Pectoral Fin Angle", default=20.0, min=0, max=80) 75 | pPecPhase : FloatProperty(name="Pec Fin Tip Phase", description="How far the fin tip lags behind the main movement in degrees", default=90.0, min=0, max=180) 76 | pPecStubRatio : FloatProperty(name="Pectoral Stub Ratio", description="Ratio for the bottom part of the pectoral fin", default=0.7, min=0, max=2) 77 | pPecStiffness : FloatProperty(name="Pec Fin Stiffness", description="Pectoral fin stiffness, with 1.0 being very stiff", default=0.7, min=0, max=2) 78 | pHTransTime : FloatProperty(name="Hover Transition Time", description="Speed of transition between swim and hover in seconds", default=0.5, min=0, max=2) 79 | pSTransTime : FloatProperty(name="Swim Transition Time", description="Speed of transition between hover and swim in seconds", default=0.2, min=0, max=2) 80 | pPecOffset : FloatProperty(name="Pectoral Offset", description="Adjustment to allow for different rest pose angles of the fins", default=20.0, min=-90.0, max=90.0) 81 | pHoverDist : FloatProperty(name="Hover Distance", description="Distance from Target to begin Hover in lengths of the target box. A value of 0 will disable hovering, and the action will be similar to the shark rig.", default=1.0, min=-1.0, max=10.0) 82 | pHoverTailFrc : FloatProperty(name="Hover Tail Fraction", description="During Hover, the amount of swimming tail movement to retain. 1.0 is full movment, 0 is none", default=0.2, min=0.0, max=5.0) 83 | pHoverMaxForce : FloatProperty(name="Hover Max Force", description="The maximum force the fins can apply in Hover Mode. 1.0 is quite fast", default=0.2, min=0.0, max=10.0) 84 | pHoverDerate : FloatProperty(name="Hover Derate", description="In hover, the fish can't go backwards or sideways as fast. This parameter determines how much slower. 1.0 is the same.", default=0.2, min=-0.0, max=1.0) 85 | pHoverTilt : FloatProperty(name="Hover Tilt", description="The amount of forward/backward tilt in hover as the fish powers forward and backward. in Degrees and based on Max Hover Force", default=4.0, min=-0.0, max=40.0) 86 | pPecDuration : FloatProperty(name="Pec Duration", description="The amount of hovering the fish can do before a rest. Duration in frames", default=50.0, min=-5.0) 87 | pPecDuty : FloatProperty(name="Pec Duty Cycle", description="The amount of rest time compared to active time. 1.0 is 50/50, 0.0 is no rest", default=0.8, min=0.0) 88 | pPecTransition : FloatProperty(name="Pec Transition to rest speed", description="The speed that the pecs change between rest and flap - 1 is instant, 0.05 is fairly slow", default=0.05, min=0.0, max=1.0) 89 | pHoverTwitch : FloatProperty(name="Hover Twitch", description="The size of twitching while in hover mode in degrees", default=4.0, min=0.0, max=60.0) 90 | pHoverTwitchTime : FloatProperty(name="Hover Twitch Time", description="The time between twitching while in hover mode in frames", default=40.0, min=0.0) 91 | pPecSynch : BoolProperty(name="Pec Synch", description="If true then fins beat together, otherwise fins act out of phase", default=False) 92 | 93 | class ARMATURE_OT_FSimulate(bpy.types.Operator): 94 | """Simulate all armatures with a similar name to selected""" 95 | bl_idname = "armature.fsimulate" 96 | bl_label = "Simulate" 97 | bl_options = {'REGISTER', 'UNDO', 'PRESET'} 98 | 99 | _timer = None 100 | sRoot = None 101 | sTorso = None 102 | sSpine_master = None 103 | sBack_fin1 = None 104 | sBack_fin2 = None 105 | sBack_fin_middle = None 106 | sChest = None 107 | sSideFinL = None 108 | sSideFinR = None 109 | #pecs 110 | sPecFinTopL = None 111 | sPecFinTopR = None 112 | sPecFinBottomL = None 113 | sPecFinBottomR = None 114 | sPecFinPalmL = None 115 | sPecFinPalmR = None 116 | sState = 0.0 117 | sPecState = 0.0 118 | sPec_scale = 1.0 119 | sAngularForceV = 0.0 120 | sTargetProxy = None 121 | rMaxTailAngle = 0.0 122 | rMaxFreq = 0.0 123 | sTargetRig = None 124 | sOldRqdEffort = 0.0 125 | sOld_back_fin = None 126 | sArmatures = [] 127 | nArmature = 0 128 | sHoverMode = 1.0 129 | sHoverTurn = 0.0 130 | sRestFrame = 0.0 131 | sRestartFrame = 0.0 132 | sRestAmount = 0.0 133 | sGoldfish = True 134 | sStartAngle = 0.0 135 | sTwitchFrame = 0.0 136 | sTwitchAngle = 0.0 137 | sTwitchTarget = 0.0 138 | 139 | def SetInitialKeyframe(self, TargetRig, nFrame): 140 | TargetRig.keyframe_insert(data_path='location', frame=(nFrame)) 141 | TargetRig.keyframe_insert(data_path='rotation_euler', frame=(nFrame)) 142 | self.sSpine_master.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 143 | self.sChest.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 144 | self.sTorso.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 145 | self.sBack_fin1.keyframe_insert(data_path='scale', frame=(nFrame)) 146 | self.sBack_fin2.keyframe_insert(data_path='scale', frame=(nFrame)) 147 | self.sSideFinL.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 148 | self.sSideFinR.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 149 | self.sSideFinR.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 150 | self.sRoot.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 151 | if self.sGoldfish: 152 | self.sPecFinPalmL.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 153 | self.sPecFinPalmR.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 154 | self.sPecFinTopL.keyframe_insert(data_path='scale', frame=(nFrame)) 155 | self.sPecFinBottomL.keyframe_insert(data_path='scale', frame=(nFrame)) 156 | self.sPecFinTopR.keyframe_insert(data_path='scale', frame=(nFrame)) 157 | self.sPecFinBottomR.keyframe_insert(data_path='scale', frame=(nFrame)) 158 | 159 | 160 | def armature_list(self, scene, sFPM): 161 | self.sArmatures = [] 162 | for obj in scene.objects: 163 | if obj.type == "ARMATURE" and obj.name[:3] == self.sTargetRig.name[:3]: 164 | root = obj.pose.bones.get("root") 165 | if root != None: 166 | if 'TargetProxy' in root: 167 | self.sArmatures.append(obj.name) 168 | self.nArmature = len(self.sArmatures) - 1 169 | # print("List: ", self.sArmatures) 170 | 171 | 172 | def RemoveKeyframes(self, armature, bones): 173 | dispose_paths = [] 174 | #print("Bones:") 175 | #dispose_paths.append('pose.bones["{}"].rotation_quaternion'.format(bone.name)) 176 | for fcurve in armature.animation_data.action.fcurves: 177 | if (fcurve.data_path == "location" or fcurve.data_path == "rotation_euler"): 178 | armature.animation_data.action.fcurves.remove(fcurve) 179 | for bone in bones: 180 | #bone.rotation_mode='XYZ' 181 | #dispose_paths.append('pose.bones["{}"].rotation_euler'.format(bone.name)) 182 | dispose_paths.append('pose.bones["{}"].rotation_quaternion'.format(bone.name)) 183 | dispose_paths.append('pose.bones["{}"].scale'.format(bone.name)) 184 | dispose_curves = [fcurve for fcurve in armature.animation_data.action.fcurves if fcurve.data_path in dispose_paths] 185 | for fcurve in dispose_curves: 186 | armature.animation_data.action.fcurves.remove(fcurve) 187 | 188 | #Set Effort and Direction properties to try and reach the target. 189 | def Target(self, TargetRig, TargetProxy, pFS): 190 | 191 | RigDirn = mathutils.Vector((0,-1,0)) @ TargetRig.matrix_world.inverted() 192 | #print("RigDirn: ", RigDirn) 193 | 194 | #distance to target 195 | if TargetProxy != None: 196 | TargetDirn = (TargetProxy.matrix_world.to_translation() - TargetRig.location) 197 | # print("TargetDirn1: ", TargetDirn) 198 | else: 199 | TargetDirn = mathutils.Vector((0,-10,0)) 200 | # print("TargetDirn2: ", TargetDirn) 201 | DifDot = TargetDirn.dot(RigDirn) 202 | 203 | #horizontal angle to target - limit max turning effort at 90 deg 204 | RigDirn2D = mathutils.Vector((RigDirn.x, RigDirn.y)) 205 | TargetDirn2D = mathutils.Vector((TargetDirn.x, TargetDirn.y)) 206 | AngleToTarget = math.degrees(RigDirn2D.angle_signed(TargetDirn2D, math.radians(180))) 207 | DirectionEffort = AngleToTarget/90.0 208 | DirectionEffort = min(1.0,DirectionEffort) 209 | DirectionEffort = max(-1.0,DirectionEffort) 210 | 211 | #vertical angle to target - limit max turning effort at 20 deg 212 | RigDirn2DV = mathutils.Vector(((RigDirn.y**2 + RigDirn.x**2)**0.5, RigDirn.z)) 213 | TargetDirn2DV = mathutils.Vector(((TargetDirn.y**2 + TargetDirn.x**2)**0.5, TargetDirn.z)) 214 | AngleToTargetV = math.degrees(RigDirn2DV.angle_signed(TargetDirn2DV, math.radians(180))) 215 | DirectionEffortV = AngleToTargetV/20.0 216 | DirectionEffortV = min(1.0,DirectionEffortV) 217 | DirectionEffortV = max(-1.0,DirectionEffortV) 218 | 219 | #Hover Mode Detection (Close to target and slow) 220 | if not self.sGoldfish: 221 | self.sHoverMode = 0.0 222 | elif TargetDirn.length < (TargetProxy.dimensions[1] * pFS.pHoverDist):# and pFS.sVelocity.length < pFS.pHoverVel: 223 | self.sHoverMode = min(1.0, self.sHoverMode + pFS.pSTransTime / 25.0) 224 | else: 225 | self.sHoverMode = max(0.0, self.sHoverMode - pFS.pHTransTime / 25.0) 226 | # print("Hover %.2f, %.2f, %.2f, %.2f" % (self.sHoverMode, TargetDirn.length, TargetProxy.dimensions[1] * 3.0, pFS.sVelocity.length)) 227 | 228 | #Return normalised required effort, turning factor, and ascending factor 229 | return DifDot,DirectionEffort,DirectionEffortV 230 | 231 | #Handle the object movement for swimming 232 | def ObjectMovment(self, TargetRig, ForwardForce, AngularForce, AngularForceV, nFrame, TargetProxy, pFS): 233 | # print("MovementSwim") 234 | #RigDirn = mathutils.Vector((0,-1,0)) * TargetRig.matrix_world.inverted() 235 | #Total force is tail force - drag 236 | # DragForce = pFS.pDrag * pFS.sVelocity ** 2.0 237 | pFS.sVelocity[0] += -(pFS.pDrag * pFS.sVelocity[0] * math.fabs(pFS.sVelocity[0])) / pFS.pMass 238 | pFS.sVelocity[1] += (-ForwardForce + -pFS.pDrag * pFS.sVelocity[1] * math.fabs(pFS.sVelocity[1])) / pFS.pMass 239 | pFS.sVelocity[2] += -(pFS.pDrag * pFS.sVelocity[2] * math.fabs(pFS.sVelocity[2])) / pFS.pMass 240 | # print("Velocity", pFS.sVelocity,pFS.pDrag,pFS.pMass) 241 | #print("Fwd, Drag: ", ForwardForce, DragForce) 242 | TargetRig.location += pFS.sVelocity @ TargetRig.matrix_world.inverted() 243 | TargetRig.keyframe_insert(data_path='location', frame=(nFrame)) 244 | 245 | #Let's be simplistic - just rotate object based on angluar force 246 | TargetRig.rotation_euler.z += math.radians(AngularForce) 247 | TargetRig.rotation_euler.x += math.radians(AngularForceV) 248 | TargetRig.keyframe_insert(data_path='rotation_euler', frame=(nFrame)) 249 | self.sHoverTurn = 0.0 250 | 251 | #Forward/Backward Tilt based on force 252 | if self.sHoverMode <= 0.1: 253 | self.sRoot.rotation_quaternion = mathutils.Quaternion((1,0,0), math.radians(0)) 254 | self.sRoot.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 255 | 256 | #Handle the object movement for hovering 257 | def ObjectMovmentHover(self, TargetRig, nFrame, TargetProxy, pFS): 258 | # print("MovementHover") 259 | RigForce = self.sHoverMode * pFS.pPecEffortGain * (TargetProxy.matrix_world.to_translation() - TargetRig.location) @ TargetRig.matrix_world 260 | 261 | #Limit the force available 262 | xHoverMaxForce = pFS.pHoverMaxForce * (1-self.sRestAmount*0.6) 263 | RigForce[1] = max(RigForce[1], -xHoverMaxForce) 264 | RigForce[1] = min(RigForce[1], xHoverMaxForce * pFS.pHoverDerate) 265 | RigForce[2] = max(RigForce[2], -xHoverMaxForce * pFS.pHoverDerate) 266 | RigForce[2] = min(RigForce[2], xHoverMaxForce * pFS.pHoverDerate) 267 | RigForce[0] = max(RigForce[0], -xHoverMaxForce * pFS.pHoverDerate) 268 | RigForce[0] = min(RigForce[0], xHoverMaxForce * pFS.pHoverDerate) 269 | 270 | #Calculate velocity 271 | pFS.sVelocity[0] += (RigForce[0] - pFS.pDrag * pFS.sVelocity[0] * math.fabs(pFS.sVelocity[0])) / pFS.pMass 272 | pFS.sVelocity[1] += (RigForce[1] - pFS.pDrag * pFS.sVelocity[1] * math.fabs(pFS.sVelocity[1])) / pFS.pMass 273 | pFS.sVelocity[2] += (RigForce[2] - pFS.pDrag * pFS.sVelocity[2] * math.fabs(pFS.sVelocity[2])) / pFS.pMass 274 | TargetRig.location += pFS.sVelocity @ TargetRig.matrix_world.inverted() 275 | TargetRig.keyframe_insert(data_path='location', frame=(nFrame)) 276 | # print("sVelocity", pFS.sVelocity) 277 | 278 | #Rotate model direction to match target 279 | # TargetRig.rotation_mode = 'QUATERNION' 280 | xTargetQuat = TargetProxy.matrix_world.to_quaternion() @ mathutils.Quaternion((0,0,1),math.radians(self.sStartAngle)) 281 | xRigQuat = TargetRig.rotation_euler.to_quaternion() 282 | xRigQuat = xRigQuat.slerp(xTargetQuat,pFS.pPecTurnAssist/100.0) 283 | TargetRig.rotation_euler = xRigQuat.to_euler('XYZ', TargetRig.rotation_euler) 284 | # TargetRig.rotation_mode = 'XYZ' 285 | TargetRig.keyframe_insert(data_path='rotation_euler', frame=(nFrame)) 286 | 287 | #Forward/Backward Tilt based on force 288 | if RigForce[1] < 0: 289 | rf = RigForce[1] * pFS.pHoverDerate 290 | else: 291 | rf = RigForce[1] 292 | TiltAngle = math.radians(pFS.pHoverTilt * rf / (pFS.pHoverMaxForce * pFS.pHoverDerate)) 293 | # self.sRoot.rotation_quaternion = mathutils.Quaternion((-1,0,0), math.radians(0)) 294 | self.sRoot.rotation_quaternion = self.sRoot.rotation_quaternion.slerp(mathutils.Quaternion((1,0,0), TiltAngle),0.03) 295 | self.sRoot.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 296 | 297 | #Get left or right turn 298 | xTurnQuat = TargetRig.rotation_quaternion @ TargetProxy.matrix_world.to_quaternion().inverted() 299 | self.sHoverTurn = math.degrees(xTurnQuat.to_euler()[2]) 300 | # print("TurnQuat:", self.sHoverTurn) 301 | 302 | #Handle the movement of the bones within the armature 303 | def BoneMovement(self, context): 304 | 305 | 306 | scene = context.scene 307 | pFS = scene.FSimProps 308 | pFSM = scene.FSimMainProps 309 | startFrame = pFSM.fsim_start_frame 310 | endFrame = pFSM.fsim_end_frame 311 | self.sStartAngle = pFSM.fsim_startangle 312 | 313 | #Get the current Target Rig 314 | # try: 315 | # print("nArmature, sAmartures: ", self.nArmature, self.sArmatures) 316 | TargetRig = scene.objects.get(self.sArmatures[self.nArmature]) 317 | # except IndexError: 318 | # TargetRig = None 319 | self.sTargetRig = TargetRig 320 | 321 | #Check the required Rigify bones are present 322 | self.sRoot = TargetRig.pose.bones.get("root") 323 | self.sTorso = TargetRig.pose.bones.get("torso") 324 | self.sSpine_master = TargetRig.pose.bones.get("spine_master") 325 | if self.sSpine_master is None: 326 | self.sSpine_master = TargetRig.pose.bones.get("spine_master.002") 327 | self.sBack_fin1 = TargetRig.pose.bones.get("back_fin_masterBk.001") 328 | if self.sBack_fin1 is None: 329 | self.sBack_fin1 = TargetRig.pose.bones.get("back_fin.T.Bk_master") 330 | self.sBack_fin2 = TargetRig.pose.bones.get("back_fin_masterBk") 331 | if self.sBack_fin2 is None: 332 | self.sBack_fin2 = TargetRig.pose.bones.get("back_fin.B.Bk_master") 333 | self.sBack_fin_middle = TargetRig.pose.bones.get("DEF-back_fin.T.001.Bk") 334 | self.sChest = TargetRig.pose.bones.get("chest") 335 | self.sSideFinL = TargetRig.pose.bones.get("side_fin.L") 336 | self.sSideFinR = TargetRig.pose.bones.get("side_fin.R") 337 | print("Shark Bone Types:", self.sTorso, self.sChest, self.sBack_fin1, self.sBack_fin2) 338 | if (self.sSpine_master is None) or (self.sTorso is None) or (self.sChest is None) or (self.sBack_fin1 is None) or (self.sBack_fin2 is None) or (self.sBack_fin_middle is None) or (self.sSideFinL is None) or (self.sSideFinR is None): 339 | self.report({'ERROR'}, "Sorry, this addon needs a Rigify rig generated from a Shark Metarig") 340 | print("Not an Suitable Rigify Armature") 341 | return 0,0 342 | 343 | # Pectoral fins if they exist 344 | self.sPecFinTopL = TargetRig.pose.bones.get("tpec_master.L") 345 | if self.sPecFinTopL is None: 346 | self.sPecFinTopL = TargetRig.pose.bones.get("t_master.L") 347 | self.sPecFinTopR = TargetRig.pose.bones.get("tpec_master.R") 348 | if self.sPecFinTopR is None: 349 | self.sPecFinTopR = TargetRig.pose.bones.get("t_master.R") 350 | self.sPecFinBottomL = TargetRig.pose.bones.get("bpec_master.L") 351 | if self.sPecFinBottomL is None: 352 | self.sPecFinBottomL = TargetRig.pose.bones.get("b_master.L") 353 | self.sPecFinBottomR = TargetRig.pose.bones.get("bpec_master.R") 354 | if self.sPecFinBottomR is None: 355 | self.sPecFinBottomR = TargetRig.pose.bones.get("b_master.R") 356 | self.sPecFinPalmL = TargetRig.pose.bones.get("pec_palm.L") 357 | self.sPecFinPalmR = TargetRig.pose.bones.get("pec_palm.R") 358 | if (self.sPecFinTopL is None) or (self.sPecFinTopR is None) or (self.sPecFinBottomL is None) or (self.sPecFinBottomR is None) or (self.sPecFinPalmL is None) or (self.sPecFinPalmR is None): 359 | print("Not a Goldfish Armature") 360 | self.sGoldfish = False 361 | self.sHoverMode = 0.0 362 | 363 | #initialise state variabiles 364 | self.sState = 0.0 365 | self.AngularForceV = 0.0 366 | self.sPecState = 0.0 367 | pFS.sVelocity[0] = pFS.sVelocity[1] = pFS.sVelocity[2] = 0.0 368 | # print("Init SVelocity", pFS.sVelocity) 369 | 370 | #Get TargetProxy object details 371 | try: 372 | TargetProxyName = self.sRoot["TargetProxy"] 373 | # print("TargetProxyName: ", TargetProxyName) 374 | self.sTargetProxy = bpy.data.objects[TargetProxyName] 375 | except: 376 | self.sTargetProxy = None 377 | 378 | print("TargetProxyName: ", self.sTargetProxy.name) 379 | #Go back to the start before removing keyframes to remember starting point 380 | context.scene.frame_set(startFrame) 381 | 382 | #Delete existing keyframes 383 | try: 384 | self.RemoveKeyframes(TargetRig, [self.sSpine_master, self.sBack_fin1, self.sBack_fin2, self.sChest, self.sSideFinL, self.sSideFinR, self.sPecFinPalmL, self.sPecFinPalmR, self.sPecFinTopL, self.sPecFinBottomL, self.sPecFinTopR, self.sPecFinBottomR, self.sRoot, self.sTorso]) 385 | except AttributeError: 386 | pass 387 | # print("info: no keyframes") 388 | 389 | #record to previous tail position 390 | context.scene.frame_set(startFrame) 391 | # context.scene.update() 392 | self.SetInitialKeyframe(TargetRig, startFrame) 393 | 394 | #randomise parameters 395 | rFact = pFS.pRandom 396 | self.rMaxTailAngle = pFS.pMaxTailAngle * (1 + (random() * 2.0 - 1.0) * rFact) 397 | self.rMaxFreq = pFS.pMaxFreq * (1 + (random() * 2.0 - 1.0) * rFact) 398 | 399 | def PecSimulation(self, nFrame, pFS, startFrame): 400 | # print("Pecs") 401 | if self.sPecFinTopL == None or self.sPecFinTopR == None: 402 | return 403 | 404 | #Update State and main angle 405 | self.sPecState = self.sPecState + 360.0 / pFS.pMaxPecFreq 406 | xPecAngle = math.sin(math.radians(self.sPecState))*math.radians(pFS.pMaxPecAngle) 407 | yPecAngle = math.sin(math.radians(self.sPecState+90.0))*math.radians(pFS.pMaxPecAngle * 2) 408 | 409 | #Rest Period Calculations 410 | 411 | if nFrame >= self.sRestartFrame: 412 | self.sRestAmount = max(0.0, self.sRestAmount - pFS.pPecTransition) 413 | if self.sRestAmount < 0.1: 414 | self.sRestFrame = nFrame + pFS.pPecDuration 415 | self.sRestartFrame = self.sRestFrame + pFS.pPecDuty * pFS.pPecDuration 416 | 417 | if (nFrame >= self.sRestFrame and nFrame < self.sRestartFrame and self.sRestAmount < 1.0): 418 | self.sRestAmount = min(1.0, self.sRestAmount + pFS.pPecTransition) 419 | 420 | # print("RestAmount: ", self.sRestAmount, self.sRestFrame, self.sRestartFrame) 421 | 422 | #Add the same side fin wobble to the pec fins to stop them looking boring when not flapping 423 | SideFinRot = math.radians(math.sin(math.radians(self.sState + pFS.pSideFinPhase)) * pFS.pMaxSideFinAngle) 424 | 425 | #Slerp between oscillating angle and rest angle depending on hover status and reset periods 426 | # xRestAmount = 1 means no flapping due to either resting or not hovering 427 | xRestAmount = (1.0 - (1.0 - self.sRestAmount) * self.sHoverMode) 428 | # print("xRestAmaount: ", xRestAmount) 429 | # print("RestAmount: ", self.sRestAmount, self.sRestFrame, self.sRestartFrame) 430 | # print("HoverMode: ", self.sHoverMode) 431 | yAng = mathutils.Quaternion((0.0, 1.0, 0.0), yPecAngle) 432 | # yAng = mathutils.Quaternion((0.0, 1.0, 0.0), 0) 433 | xAng = yAng @ mathutils.Quaternion((1.0, 0.0, 0.0), -xPecAngle) 434 | xAng = xAng.slerp(mathutils.Quaternion((1.0, 0.0, 0.0), math.radians(pFS.pPecOffset)), xRestAmount) 435 | self.sPecFinPalmL.rotation_quaternion = xAng @ mathutils.Quaternion((1.0, 0.0, 0.0), SideFinRot) 436 | self.sPecFinPalmL.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 437 | # print("Palm Animate: ", nFrame) 438 | 439 | #Tip deflection based on phase offset 440 | xMaxPecScale = pFS.pMaxPecAngle * ( 1.0 / pFS.pPecStiffness) * 0.2 / 30.0 441 | 442 | self.sPec_scale = 1.0 + math.sin(math.radians(self.sPecState - pFS.pPecPhase)) * xMaxPecScale * (1.0 - xRestAmount) 443 | 444 | self.sPecFinTopL.scale[1] = self.sPec_scale 445 | self.sPecFinBottomL.scale[1] = 1 - (1 - self.sPec_scale) * pFS.pPecStubRatio 446 | self.sPecFinTopL.keyframe_insert(data_path='scale', frame=(nFrame)) 447 | self.sPecFinBottomL.keyframe_insert(data_path='scale', frame=(nFrame)) 448 | 449 | #copy to the right fin 450 | 451 | #If fins are opposing 452 | if not pFS.pPecSynch: 453 | yAng = mathutils.Quaternion((0.0, 1.0, 0.0), yPecAngle) 454 | xAng = yAng @ mathutils.Quaternion((1.0, 0.0, 0.0), xPecAngle) 455 | xAng = xAng.slerp(mathutils.Quaternion((1.0, 0.0, 0.0), math.radians(pFS.pPecOffset)), xRestAmount) 456 | self.sPecFinPalmR.rotation_quaternion = xAng @ mathutils.Quaternion((1.0, 0.0, 0.0), SideFinRot) 457 | self.sPecFinTopR.scale[1] = 1/self.sPec_scale 458 | self.sPecFinBottomR.scale[1] = 1 - (1 - 1/self.sPec_scale) * pFS.pPecStubRatio 459 | else: 460 | yAng = mathutils.Quaternion((0.0, 1.0, 0.0), -yPecAngle) 461 | xAng = yAng @ mathutils.Quaternion((1.0, 0.0, 0.0), -xPecAngle) 462 | xAng = xAng.slerp(mathutils.Quaternion((1.0, 0.0, 0.0), math.radians(pFS.pPecOffset)), xRestAmount) 463 | self.sPecFinPalmR.rotation_quaternion = xAng @ mathutils.Quaternion((1.0, 0.0, 0.0), SideFinRot) 464 | self.sPecFinTopR.scale[1] = self.sPec_scale 465 | self.sPecFinBottomR.scale[1] = 1 - (1 - self.sPec_scale) * pFS.pPecStubRatio 466 | self.sPecFinPalmR.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 467 | self.sPecFinTopR.keyframe_insert(data_path='scale', frame=(nFrame)) 468 | self.sPecFinBottomR.keyframe_insert(data_path='scale', frame=(nFrame)) 469 | 470 | 471 | 472 | def ModalMove(self, context): 473 | scene = context.scene 474 | pFS = scene.FSimProps 475 | pFSM = scene.FSimMainProps 476 | startFrame = pFSM.fsim_start_frame 477 | endFrame = pFSM.fsim_end_frame 478 | 479 | nFrame = scene.frame_current 480 | # print("nFrame: ", nFrame) 481 | 482 | 483 | #Get the effort and direction change to head toward the target 484 | RqdEffort, RqdDirection, RqdDirectionV = self.Target(self.sTargetRig, self.sTargetProxy, pFS) 485 | if nFrame == startFrame: 486 | self.sOldRqdEffort = RqdEffort 487 | context.scene.frame_set(nFrame + 1) 488 | self.sOld_back_fin = self.sBack_fin_middle.matrix.decompose()[0] 489 | return 1 490 | TargetEffort = pFS.pEffortGain * (pFS.pEffortIntegral * RqdEffort + (RqdEffort - self.sOldRqdEffort)) 491 | self.sOldRqdEffort = RqdEffort 492 | pFS.sEffort = pFS.pEffortGain * RqdEffort * pFS.pEffortRamp + pFS.sEffort * (1.0-pFS.pEffortRamp) 493 | pFS.sEffort = min(pFS.sEffort, 1.0) 494 | #print("Required, Effort:", RqdEffort, pFS.sEffort) 495 | 496 | #Pec fin simulation 497 | self.PecSimulation(nFrame, pFS, startFrame) 498 | 499 | #Convert effort into tail frequency and amplitude (Fades to a low value if in hover mode) 500 | pFS.pFreq = self.rMaxFreq * ((1-self.sHoverMode) * (1.0/(pFS.sEffort+ 0.01)) + self.sHoverMode * 2.0) 501 | pFS.pTailAngle = self.rMaxTailAngle * ((1-self.sHoverMode) * pFS.sEffort + self.sHoverMode * pFS.pHoverTailFrc) 502 | #print("rMax, Frc: %.2f, %.2f" % (self.rMaxTailAngle, pFS.pHoverTailFrc)) 503 | 504 | #Convert direction into Tail Offset angle (Work out swim turn angle and Hover turn angle and mix) 505 | xSwimTailAngleOffset = RqdDirection * pFS.pMaxSteeringAngle 506 | xHoverTailAngleOffset = pFS.pMaxSteeringAngle * self.sHoverTurn / 30.0 507 | xHoverTailAngleOffset = 0.0 508 | # xHoverFactor = max(0,(1.0 - self.sHoverMode * 4.0)) 509 | pFS.sTailAngleOffset = pFS.sTailAngleOffset * (1 - pFS.pEffortRamp) + pFS.pEffortRamp * max(0,(1.0 - self.sHoverMode*2.0)) * xSwimTailAngleOffset + pFS.pEffortRamp * self.sHoverMode * xHoverTailAngleOffset 510 | # pFS.sTailAngleOffset = pFS.sTailAngleOffset * (1 - pFS.pEffortRamp) + pFS.pEffortRamp * xSwimTailAngleOffset 511 | # print("xHoverOffset, TailOffset: ", xHoverTailAngleOffset, pFS.sTailAngleOffset) 512 | # print("HoverMode, xSwimTailAngleOffset: ", self.sHoverMode, xSwimTailAngleOffset) 513 | 514 | #Hover 'Twitch' calculations (Make the fish do some random twisting during hover mode) 515 | if self.sHoverMode < 0.5: 516 | #Not hovering so reset 517 | self.sTwitchTarget = 0.0 518 | self.sTwitchFrame = 0.0 519 | else: 520 | #Hovering, so check if the twitch frame has been reached 521 | if nFrame >= self.sTwitchFrame: 522 | #set new twitch frame 523 | self.sTwitchFrame = nFrame + pFS.pHoverTwitchTime * (random() - 0.5) 524 | #Only twitch while not resting 525 | if self.sTwitchFrame < self.sRestartFrame and self.sTwitchFrame > self.sRestFrame: 526 | self.sTwitchFrame = self.sRestartFrame + 5 527 | #set a new twitch target angle 528 | self.sTwitchTarget = pFS.pHoverTwitch * 2.0 * (random() - 0.5) 529 | self.sTwitchAngle = self.sTwitchAngle * 0.9 + 0.1 * self.sTwitchTarget 530 | #print("Twitch Angle: ", self.sTwitchAngle) 531 | 532 | 533 | 534 | #Spine Movement 535 | self.sState = self.sState + 360.0 / pFS.pFreq 536 | xTailAngle = math.sin(math.radians(self.sState))*math.radians(pFS.pTailAngle) + math.radians(pFS.sTailAngleOffset) + math.radians(self.sTwitchAngle) 537 | #print("Components: %.2f, %.2f, %.2f" % (math.sin(math.radians(self.sState))*math.radians(pFS.pTailAngle),math.radians(pFS.sTailAngleOffset),math.radians(self.sTwitchAngle))) 538 | #print("TailAngle", math.degrees(xTailAngle)) 539 | self.sSpine_master.rotation_quaternion = mathutils.Quaternion((0.0, 0.0, 1.0), xTailAngle) 540 | self.sSpine_master.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 541 | ChestRot = mathutils.Quaternion((0.0, 0.0, 1.0), -xTailAngle * pFS.pChestRatio)# - math.radians(pFS.sTailAngleOffset)) 542 | self.sChest.rotation_quaternion = ChestRot @ mathutils.Quaternion((1.0, 0.0, 0.0), -math.fabs(math.radians(pFS.sTailAngleOffset))*pFS.pChestRaise * (1.0 - self.sHoverMode)) 543 | #print("Torso:", pFS.sTailAngleOffset) 544 | self.sTorso.rotation_quaternion = mathutils.Quaternion((0.0, 1.0, 0.0), -math.radians(pFS.sTailAngleOffset)*pFS.pLeanIntoTurn * (1.0 - self.sHoverMode)) 545 | self.sChest.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 546 | self.sTorso.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 547 | #context.scene.update() 548 | 549 | # #Tail Movment 550 | if (nFrame == startFrame): 551 | back_fin_dif = 0 552 | else: 553 | back_fin_dif = (self.sBack_fin_middle.matrix.decompose()[0].x - self.sOld_back_fin.x) 554 | self.sOld_back_fin = self.sBack_fin_middle.matrix.decompose()[0] 555 | 556 | #Tailfin bending based on phase offset 557 | pMaxTailScale = pFS.pMaxTailFinAngle * ( 1.0 / pFS.pTailFinStiffness) * 0.2 / 30.0 558 | 559 | self.sBack_fin1_scale = 1.0 + math.sin(math.radians(self.sState + pFS.pTailFinPhase)) * pMaxTailScale * (pFS.pTailAngle / self.rMaxTailAngle) 560 | # print("Bend Factor: ", (pFS.pTailAngle / self.rMaxTailAngle)) 561 | 562 | self.sBack_fin1.scale[1] = self.sBack_fin1_scale 563 | self.sBack_fin2.scale[1] = 1 - (1 - self.sBack_fin1_scale) * pFS.pTailFinStubRatio 564 | self.sBack_fin1.keyframe_insert(data_path='scale', frame=(nFrame)) 565 | self.sBack_fin2.keyframe_insert(data_path='scale', frame=(nFrame)) 566 | 567 | 568 | SideFinRot = math.sin(math.radians(self.sState + pFS.pSideFinPhase)) * pFS.pMaxSideFinAngle 569 | 570 | 571 | self.sSideFinL.rotation_quaternion = mathutils.Quaternion((1,0,0), math.radians(-SideFinRot)) 572 | self.sSideFinR.rotation_quaternion = mathutils.Quaternion((1,0,0), math.radians(SideFinRot)) 573 | 574 | self.sSideFinL.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 575 | self.sSideFinR.keyframe_insert(data_path='rotation_quaternion', frame=(nFrame)) 576 | 577 | #Do Object movment with Forward force and Angular force 578 | TailFinAngle = (self.sBack_fin1_scale - 1.0) * 30.0 / 0.4 579 | TailFinAngleForce = math.sin(math.radians(TailFinAngle)) 580 | # ForwardForce = -back_fin_dif * TailFinAngleForce * pFS.pPower 581 | ForwardForce = math.fabs(math.cos(math.radians(self.sState))) * math.radians(pFS.pTailAngle) * 15.0 * pFS.pPower / pFS.pMaxFreq 582 | # print("Force", ForwardForce, math.fabs(math.cos(math.radians(self.sState))), math.radians(pFS.pTailAngle)) 583 | 584 | #Angular force due to 'swish' 585 | AngularForce = back_fin_dif / pFS.pAngularDrag 586 | 587 | #Angular force due to rudder effect 588 | AngularForce += xTailAngle * pFS.sVelocity[1] / pFS.pAngularDrag 589 | 590 | #Fake Angular force to make turning more effective 591 | AngularForce += -(pFS.sTailAngleOffset/pFS.pMaxSteeringAngle) * pFS.pTurnAssist 592 | 593 | #Angular force for vertical movement 594 | self.sAngularForceV = self.sAngularForceV * (1 - pFS.pEffortRamp) + RqdDirectionV * pFS.pMaxVerticalAngle 595 | 596 | 597 | #print("TailFinAngle, AngularForce", xTailAngle, AngularForce) 598 | if self.sHoverMode < 0.1: 599 | self.ObjectMovment(self.sTargetRig, ForwardForce, AngularForce, self.sAngularForceV, nFrame, self.sTargetProxy, pFS) 600 | else: 601 | self.ObjectMovmentHover(self.sTargetRig, nFrame, self.sTargetProxy, pFS) 602 | 603 | 604 | #Go to next frame, or finish 605 | wm = context.window_manager 606 | # print("Frame: ", nFrame) 607 | if nFrame == endFrame: 608 | return 0 609 | else: 610 | wm.progress_update((len(self.sArmatures) - self.nArmature)*99.0/len(self.sArmatures)) 611 | context.scene.frame_set(nFrame + 1) 612 | return 1 613 | 614 | 615 | def modal(self, context, event): 616 | if event.type in {'RIGHTMOUSE', 'ESC'}: 617 | self.cancel(context) 618 | return {'CANCELLED'} 619 | 620 | if event.type == 'TIMER': 621 | modal_rtn = self.ModalMove(context) 622 | if modal_rtn == 0: 623 | # print("nArmature:", self.nArmature) 624 | #Go to the next rig if applicable 625 | context.scene.frame_set(context.scene.FSimMainProps.fsim_start_frame) 626 | if self.nArmature > 0: 627 | self.nArmature -= 1 628 | self.BoneMovement(context) 629 | 630 | else: 631 | wm = context.window_manager 632 | wm.progress_end() 633 | return {'CANCELLED'} 634 | 635 | return {'PASS_THROUGH'} 636 | 637 | def execute(self, context): 638 | sFPM = context.scene.FSimMainProps 639 | # print("Power", context.scene.FSimProps.pPower) 640 | # try: 641 | # self.sTargetRig = scene.objects.get(sFPM.fsim_targetrig) 642 | # except: 643 | self.sTargetRig = context.object 644 | scene = context.scene 645 | 646 | #Load a list of the relevant armatures 647 | self.armature_list(scene, sFPM) 648 | 649 | #Progress bar 650 | wm = context.window_manager 651 | wm.progress_begin(0.0,100.0) 652 | 653 | scene.frame_set(sFPM.fsim_start_frame) 654 | self.BoneMovement(context) 655 | wm = context.window_manager 656 | self._timer = wm.event_timer_add(0.001, window=context.window) 657 | wm.modal_handler_add(self) 658 | return {'RUNNING_MODAL'} 659 | 660 | def cancel(self, context): 661 | wm = context.window_manager 662 | wm.event_timer_remove(self._timer) 663 | 664 | 665 | #Register 666 | 667 | classes = ( 668 | FSimProps, 669 | ARMATURE_OT_FSimulate, 670 | ) 671 | 672 | def registerTypes(): 673 | from bpy.utils import register_class 674 | 675 | # Classes. 676 | for cls in classes: 677 | register_class(cls) 678 | # bpy.utils.register_class(FSimProps) 679 | bpy.types.Scene.FSimProps = bpy.props.PointerProperty(type=FSimProps) 680 | # bpy.utils.register_class(ARMATURE_OT_FSimulate) 681 | 682 | def unregisterTypes(): 683 | from bpy.utils import unregister_class 684 | 685 | del bpy.types.Scene.FSimProps 686 | 687 | # Classes. 688 | for cls in classes: 689 | unregister_class(cls) 690 | # bpy.utils.unregister_class(FSimProps) 691 | # bpy.utils.unregister_class(ARMATURE_OT_FSimulate) 692 | 693 | 694 | if __name__ == "__main__": 695 | register() 696 | 697 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FishSim - 4.02 Version 2 | 3 | #### Fish Swimming Simulation 4 | V4.02.0 Release for Blender 4.2 5 | 6 | Addon Download : [FishSim_4_02_0.zip](https://github.com/nerk987/FishSim/releases/download/v4.02.0/FishSim_4_02_0.zip) 7 | 8 | 9 | ## New in This Release 10 | v4.02.0 11 | Release for Blender 4.2 and it's associated extensions website 12 | In this version, the fish models are distributed with the addon, and can be added directly. The models have been extensively re-worked to suit the updated Rigify addon. 13 | 14 | v4.00.0 15 | Release for Blender 4.00 - new bone collections 16 | 17 | v0.3.2 18 | Release for Blender 2.81 - adapting to a few more bone name changes in Rigify 19 | 20 | v0.3.1 21 | Release for Blender 2.8 Official - a few API changes and adapting to a few bone name changes in Rigify 22 | 23 | v0.3.0 24 | Initial attempt to convert for Blender 2.8 25 | (The Blender 2.8 API is a bit of a moving target at the moment, and it hasn't been tested thoroughly) 26 | 27 | 28 | v0.2.1 29 | Added body movement to pectoral fins while swimming or resting to look more natural 30 | 31 | v0.2.0 32 | * New Goldfish Rigify metarig available when both FishSim and Rigify are enabled 33 | * The simulation will now optionally allow the fish to 'hover' using pectoral fin oscillation and air bladder. 34 | * The new Pectoral Fin and Hover parameters are enabled when a rig created from the 'GoldFish' metarig is selected. 35 | 36 | 37 | ## Introduction 38 | Most computer animation is done manually by setting keyframes at key points, and then tweaking intermediate frames. For some types of animation however, it should be possible for the computer to do the detail work once the animator has set up the appropriate environment. Bullet physics and the fracture modifier are examples. Physically based animation! 39 | 40 | This addon aims to make it easier to animate natural movements of fish by allowing the animation of targets or proxies, and then simulating the movements required for the fish to follow these targets. It would be hard to support every type of rig that could be built for a fish model, but the shark metarig supplied with the built-in rigify addon provides a convenient standard that should be easy to apply to most models. 41 | 42 | In the real world, fish often come in large numbers. There is already an impressive addon called Crowdmaster which is designed to move large numbers of objects according to complex strategies. It's very well suited to setup an initial pattern for a school of fish and then animate the motion according to flocking, path following, object avoiding rules. In fact, it could handle the whole task including specifying armature actions to simulate swimming but the actions would end up somewhat robotic. Instead, it can be used to drive the motion of the targets, and the FishSim addon can then be used to produce realistic swimming physics for the 'actors' to follow the targets. An example crowdmaster setup suitable for this addon is provided. The 'animation nodes' addon is an alternative. 43 | 44 | ## Workflow summary 45 | * Create or download a fish model 46 | * Use the Rigify Shark, or FishSim/Rigify Goldfish metarig to rig the model (or download one of the example models) 47 | * Install and enable the FishSim addon. The FishSim panel will be available while the fish armature is selected 48 | * From the FishSim tools panel, add a target for the model 49 | * Animate the target via keyframes, path follow etc to show the fish model where to swim 50 | * From the FishSim tools panel, select the range of frames and select 'Simulate' 51 | * Run the animation (Alt-A), and tweak the simulation parameters to provide the desired swimming action 52 | 53 | The above workflow can be used to animate as many fish models as you like. However, for large numbers, some additional steps can make this easier. 54 | 55 | * Use the workflow above to create a fish model with a FishSim target object. 56 | * Tweak the simulation parameters to suit the planned speed of the targets. 57 | * Duplicate and animate the target as many times and with whatever tools you like. The Crowdmaster addon is very suitable, as is the Animation Nodes addon. 58 | * Select the 'Distribute multiple copies of the rig' option and then click on the 'Copy Models' button to make a copy of the fish armature at every target object location. Optionally limit the number of rigs to a small number using the 'Maximum number of copies' parameter to speed up the initial testing. 59 | * Select the 'Simulate' option again to simulate the swimming action for every matching armature and every target object location. Again tweak the swimming simulation parameters. 60 | * Select the 'Copy Meshes' option (and untick the other options) and use the 'Copy Models' button to make a copy of the fish mesh object(s) attached to the armatures at every target object location. 61 | 62 | ## Reference 63 | ### Installation 64 | It's like just about every other Blender addon. Download the file here: 65 | 66 | From the Blender file menu, choose 'User Preferences'. Switch to the 'Add-ons' tab, then select 'Install Add-on from file'. Browse to the location of the downloaded addon zip file. Enable 'FishSim' from the list of add-ons. 67 | 68 | There should be a 'FishSim' tab on the Toolbar to the left of the 3D view whenever an armature is selected in object or pose mode. 69 | 70 | 71 | ### Rigify Shark Metarig 72 | 73 | If you haven't used it, Rigify is a hugely powerful rig generator add-on that is supplied with Blender. When enabled, the 'Add Armature' menu is extended to allow the creation of various metarigs. It now supports a 'Shark' metarig, and after you adjust the metarig to suit your model, Rigify can generate a fully functioning control rig. 74 | 75 | The FishSim add-on doesn't need Rigify to be installed to work, but it does expect the rig to have been generated with Rigify from the standard Shark metarig. You can download a shark model rigged in a suitable way from here: or make your own. Even if you have a model rigged using a different method, it should be quite easy to re-rig it using Rigify. 76 | 77 | With version V0.2 of the FishSim addon, there is now a 'Goldfish' metarig available under the add/armature/fishsim/ menu next to the standard Rigify options. It's disabled unless the Rigify addon is enabled. This metarig is essentially the same as the shark metarig, except for more detailed and controllable pectoral fins. 78 | 79 | One thing I've found with these rigs is that they look better if the 'curvature' parameter on the top and bottom part of the tail fin is set to 1.0 (makes the bendy bones fully bendy). Also the Head, Neck and Tail 'Follow' paramters should be set to 1.0 or at least fairly high. In the case of the Goldfish rig, the top and bottom of each the pectoral fins should also have curvature set to 1. 80 | 81 | ### FishSim Tools Panel 82 | Once FishSim is loaded and enabled, a FishSim tab should appear on the Tool Panel on the left of a 3D view if an armature is selected in Pose or Object mode. The tab contains a 'FishSim' panel, and a 'Main Simulations Properties' panel. If the selected rig was made from the 'Goldfish' metarig, then a 'Pectoral Fins Properties' panel will also be present. 83 | 84 | ![Tools Panel](images/FSim_ToolPanel.png) 85 | 86 | 1. Animation Range 87 | 88 | >The swimming motion of the fish armatures will be animated by setting keyframes between the 'Simulation Start Frame' and the 'Simulation End Frame'. 89 | 90 | 2. Add a Target 91 | 92 | >The 'Add a Target' button will add a 'Target Proxy' object set to the bounding box size of the armature, and with wire frame display enabled. The idea then is to animate the motion of the Target Proxy, and then the fish armature can be automatically simulated to follow the target. 93 | 94 | >A 'custom property' is added to the root bone of the fish armature so that the fish knows which target to follow. The Target Proxy also gets a custom property to identify it, and this tag includes the first three letters of the rig - we'll mention this later as it helps when you have different types of fish. 95 | 96 | 3. Simulate 97 | 98 | > The Simulate function will animate various bones within the rig and add a keyframe for each frame in the animation range. The animation will try to make the rig swim in a realistic way to keep pace with the target animation, according to a number of parameters which can be adjusted in the operator re-do panel. If there are multiple rigs in the scene with the same first three characters in the name, all of the rigs will be animated. This applies specifically to the rigs generated by the following functions. 99 | 100 | 4. Simulate for multiple targets 101 | 102 | >4.1. Distribute Multiple Copies of the Rig 103 | 104 | >If this option is ticked, when the 'Copy Models' button is pressed the addon will search for every target in the scene that matches the currently selected armature. For each target, a copy of the currently selected armature will be positioned at the target's location and rotation, and linked to follow that target. The armature root bone will be scaled to match the targets scale. If an armature is already linked to that target, it's position, rotation, and root bone scale is re-adjusted to match the target. All of the copied armatures are left selected to make them easier to delete or be moved to other layers. 105 | 106 | >4.2. Distribute Multiple Copies of meshes. 107 | 108 | >If this option is ticked, when the 'Copy Models' button is pressed the addon will search for every target in the scene that matches the currently selected armature. For each target that has a linked armature, a copy of all the mesh children of the currently selected armature will be attached to the associated armature and linked. All of the copied meshes are left selected to make them easier to delete or be moved to other layers. 109 | 110 | >4.3. Maximum number of copies 111 | 112 | >The maximum number of armatures or meshes copied and/or simulated can be limited by this parameter to simplify the process of tuning the swimming action to the animated targets. 113 | 114 | >4.5. Angle to target 115 | 116 | >I found that most of the Crowd Master examples moved the objects in the positive Y direction by default, and Rigify and most models face the negative Y direction. This parameter lets you add a rotation offset when the armatures are attached to the targets. If you find your models start swimming in the opposite direction to the target, put 180.0 in this parameter. 117 | 118 | ### Simulation Parameters 119 | 120 | >Parameters affecting the swimming action of the fish can be found in the 'Main Simulation Parameters' panel after running the simulation. They can be adjusted as required to allow the model to better follow the target. The parameters are saved with the blend file, and different sets of parameters can be saved using the presets control. 121 | 122 | >If the rig has been created from the 'Goldfish' metarig, then the 'Pectoral Fin Properties' panel is visible, and contains parameters for controlling how the pectoral fins move and how the dynamics of the hovering movement. 123 | 124 | >There are a lot of parameters, but in most cases only a have to be changed. For a shark, I would suggest you setup a single armature with a target animated to move at a steady speed. Run the simulation for say 100 frames. If the model lags behind the target, decrease the tail 'Stroke Period' (flap faster) and/or increase the 'Power' (more push per tail flap). If the model overshoots the target repeatedly, or loops in a 360, do the opposite. To make the model turn faster, increase the 'Turn Assist' parameter. For a 'Goldfish' type rig, it might pay to make the target move in a jerkier way to get the effect of bursts of speed, then a hovering period. 125 | 126 | >The 'Mass' and 'Drag' parameters can be adjusted to affect the stopping speed, and the steadiness of the movement. 127 | 128 | >For the Goldfish rig, the 'Pectoral Fin Properties' panel is available. The 'Hover Mode Params' box is probably the most significant here. The 'Hover Distance' determines how close to the target the rig has to be in order to start transitioning from swimming to hovering. (The units are in lengths of the target box.) The 'Hover Transition Time' and the 'Swim Transition Time' control how quickly (in frames) the fish changes from swimming to hovering and back again. 129 | 130 | >The 'Variation Tuning' for the Goldfish model is also useful to tweak. The 'Pec Duration' and 'Pec duty cycle' allow the hovering action to take place is bursts of activity and then rest. A duty cycle of 1 allows for 50% of the time hovering with fin action and 50% floating. A duty cycle of 0 will cause the fish to paddle with the pectoral fins all the time during hovering. The 'Twitch' settings cause the fish to twist a little randomly every now and then while hovering. 131 | 132 | ### Main Simulation Parameter Reference 133 | 134 | 135 | * Mass 136 | 137 | >A higher value will make the motion steadier. A lower value will cause the speed to pulse as the tail moves back and forwards. 138 | 139 | * Drag 140 | 141 | >A higher value will allow the fish to stop quicker and may cause the speed to pulse. A higher 'Power' will be needed to obtain the same top speed. A lower value will allow the fish to glide with little tail movement. 142 | 143 | * Power 144 | 145 | >Is the amount of forward force for a given tail movement. A higher value will give a higher top speed. A value too high or low will look unrealistic. 146 | 147 | * Stroke Period 148 | 149 | >When the fish needs maximum speed to keep up, this paramter sets the number of frames per flap of the tail. The smaller the number, the faster the flapping. The faster the flapping, the faster the fish will move. 150 | 151 | * Effort Gain 152 | 153 | >The further from the target, the faster and bigger the tail will flap. The larger this value, the faster the fish will swim for the same distance from the target. If it's too big, the fish will overshoot repeatedly, too small and the fish will lag. A value of 0.2 will normally suit, a bit bigger if you want the fish right up on the target. 154 | 155 | * Effort Integral 156 | 157 | >Similar to the Effort Gain, but takes into account how long the fish has been lagging. 158 | 159 | * Effort Ramp 160 | 161 | >This affects how long it takes the fish to wind up an down. A large shark will have a slow ramp. A value of 1.0 is instantaneous, while a value of 0.05 is quite slow. 162 | 163 | * AngularDrag 164 | 165 | >A low Angular Drag will cause the direction of the fish to 'zig zag' as the tail flaps, while a high value will make the direction unaffected by every flap. The amount of direction movement also depends on the Stroke Period and Max Tail angle. 166 | 167 | * TurnAssist 168 | 169 | >A high value will allow the fish to turn fast if required, and low value will give a large turning circle. Tune this value to allow the fish to track the target while still being realistic. 170 | 171 | * Max Tail Angle 172 | 173 | >This is the angle in degrees that the tail moves from side to side during periods of maximum effort. A larger value will give a higher maximum speed. 174 | 175 | * Max Steering Angle 176 | 177 | >This is the additional tail angle used to steer the fish 178 | 179 | * Max Vertical Angle 180 | 181 | >This parameter is the most angle change in degrees that can occur in the vertical direction (ie up and down) every frame. A bigger value will give a faster response to up and down changes in the target motion. 182 | 183 | * Max Tail Fin Angle 184 | 185 | >The maximum angle in degrees that the tip and the top of the tail fin can move due to water resistance as the tail goes from side to side. Combined with the Tail Fin Gain and Tail Fin Stiffness parameters determines how stiff or floppy the tail fin is. 186 | 187 | * Tail Fin Tail Fin Phase 188 | 189 | >This parameter represents the drag of the tail fin tip. The tip of the tail fin will lag behind the main part of the fin, and this lag is represented in degrees. 90 degrees should be about right, but it can be adjusted a bit up or down if the 'floppiness' of the tail fin doesn't look right. 190 | 191 | * Tail Fin Stiffness 192 | 193 | >The stiffness is the force trying to return the fin tip to the original shape. A value of 1.0 will be very stiff, a value of 0.1 will be very bendy. 194 | 195 | * Tail Fin Stub Ratio 196 | 197 | >A value of one will make the lower part of the tail fin respond the same as the top part. A value of 0.5 will make the lower part of the tail fin move half as much. This is suitable for a shark tail where the top tip of the tail fin is bigger and longer. 198 | 199 | * Max Side Fin Angle 200 | 201 | >Like the tail fin, but for the large side fins . 202 | 203 | * Side Fin Phase 204 | 205 | >Like the tail fin, but for the large side fins 206 | 207 | * Chest Ratio 208 | 209 | >As the tail moves from side to side, the chest and head will also move. If the ratio is 1.0, the movement will be the same as the tail. A factor of 0.5 will make the chest angle half that of the tail. 210 | 211 | * Chest Raise Factor 212 | 213 | >As the fish turns, it's natural for the head and chest to arch up. A value of 0.0 will cause no effect. A larger value will determine how much chest/head raise occurs. 214 | 215 | * LeanIntoTurn 216 | 217 | >This paramter affects how much the fish leans into a turn. A factor of 0.0 will be no lean. 218 | 219 | * Random 220 | 221 | >A number of the parameters can be adjusted by a random factor. If this parameter is 0.0 then there is no random influence. The default figure of 0.25 allows paramters including the Max Tail Angle and Power to be adjusted up or down as much as 25%. This is useful when simulating a school of fish to give variation in speed and turning. 222 | 223 | ### Pectoral Fin (and Hover) Parameter Reference 224 | 225 | * Pectoral Effort Gain 226 | 227 | >Amount of effort to maintain position with 1.0 trying very hard to maintain 228 | 229 | * Pectoral Turn Assist 230 | 231 | >Turning Speed while hovering 5 is fast, .2 is slow 232 | 233 | * Pectoral Stroke Period 234 | 235 | >Maximum frequency of pectoral fin movement in frames per cycle 236 | 237 | * Max Pec Fin Angle 238 | 239 | >Max Pectoral Fin Angle 240 | 241 | * Pec Fin Tip Phase 242 | 243 | >How far the fin tip lags behind the main movement in degrees 244 | 245 | * Pectoral Stub Ratio 246 | 247 | >Ratio for the bottom part of the pectoral fin 248 | 249 | * Pec Fin Stiffness 250 | 251 | >Pectoral fin stiffness, with 1.0 being very stiff 252 | 253 | * Hover Transition Time 254 | 255 | >Speed of transition between swim and hover in seconds 256 | 257 | * Swim Transition Time 258 | 259 | >Speed of transition between hover and swim in seconds 260 | 261 | * Pectoral Offset 262 | 263 | >Adjustment to allow for different rest pose angles of the fins 264 | 265 | * Hover Distance 266 | 267 | >Distance from Target to begin Hover in lengths of the target box. A value of 0 will disable hovering, and the action will be similar to the shark rig. 268 | 269 | * Hover Tail Fraction 270 | 271 | >During Hover, the amount of swimming tail movement to retain. 1.0 is full movment, 0 is none 272 | 273 | * Hover Max Force 274 | 275 | >The maximum force the fins can apply in Hover Mode. 1.0 is quite fast 276 | 277 | * Hover Derate 278 | 279 | >In hover, the fish can't go backwards or sideways as fast. This parameter determines how much slower. 1.0 is the same. 280 | 281 | * Hover Tilt 282 | 283 | >The amount of forward/backward tilt in hover as the fish powers forward and backward. in Degrees and based on Max Hover Force 284 | 285 | * Pec Duration 286 | 287 | >The amount of hovering the fish can do before a rest. Duration in frames 288 | 289 | * Pec Duty Cycle 290 | 291 | >The amount of rest time compared to active time. 1.0 is 50/50, 0.0 is no rest 292 | 293 | * Pec Transition 294 | 295 | >The speed that the pecs change between rest and flap - 1 is instant, 0.05 is fairly slow 296 | 297 | * Hover Twitch 298 | 299 | >The size of twitching while in hover mode in degrees" 300 | 301 | * Hover Twitch Time 302 | 303 | >The time between twitching while in hover mode in frames 304 | 305 | * Pec Synch 306 | 307 | >If true then fins beat together, otherwise fins act out of phase 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # fishsim.py -- a script to apply a fish swimming simulation to an armature 4 | # by Ian Huish (nerk) 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU General Public License 8 | # as published by the Free Software Foundation; either version 2 9 | # of the License, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software Foundation, 18 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | # 20 | # ##### END GPL LICENSE BLOCK ##### 21 | 22 | # version comment: V4.02.0 - Goldfish Version - Blender 4.20 Extensions 23 | 24 | bl_info = { 25 | "name": "FishSim", 26 | "author": "Ian Huish (nerk)", 27 | "version": (4, 2, 0), 28 | "blender": (4, 2, 0), 29 | "location": "Toolshelf>FishSim", 30 | "description": "Apply fish swimming action to a Rigify Shark armature", 31 | "warning": "", 32 | "wiki_url": "http://github.com/nerk987/FishSim", 33 | "tracker_url": "http://github.com/nerk987/FishSim/issues", 34 | "category": "Animation"} 35 | 36 | 37 | if "bpy" in locals(): 38 | import imp 39 | imp.reload(FishSim) 40 | imp.reload(metarig_menu) 41 | # print("Reloaded multifiles") 42 | else: 43 | from . import FishSim 44 | # print("Imported multifiles") 45 | 46 | import bpy 47 | import mathutils, math, os 48 | from bpy.props import FloatProperty, IntProperty, BoolProperty, EnumProperty, StringProperty 49 | from random import random 50 | from bpy.types import Operator, Panel, Menu 51 | from bl_operators.presets import AddPresetBase 52 | # import shutil 53 | 54 | # print(sys.modules[bpy.types.DATA_PT_rigify_buttons.__module__].__file__) 55 | 56 | #Globals 57 | 58 | global_preset_subdir = "../../extensions/user_default/FishSim/presets" 59 | 60 | 61 | #def add_preset_files(): 62 | # presets = bpy.utils.user_resource('SCRIPTS', path="presets") 63 | # mypresets = os.path.join(presets, "operator\\fishsim") 64 | # if not os.path.exists(mypresets): 65 | # os.makedirs(mypresets) 66 | # # print("Presets dir added:", mypresets) 67 | # mypath = os.path.join(mypresets, "myfile.xxx") 68 | 69 | 70 | class FSimMainProps(bpy.types.PropertyGroup): 71 | fsim_targetrig : StringProperty(name="Name of the target rig", default="") 72 | fsim_start_frame : IntProperty(name="Simulation Start Frame", default=1) 73 | fsim_end_frame : IntProperty(name="Simulation End Frame", default=250) 74 | fsim_maxnum : IntProperty(name="Maximum number of copies", default=250) 75 | fsim_copyrigs : BoolProperty(name="Distribute multiple copies of the rig", default=True) 76 | fsim_copymesh : BoolProperty(name="Distribute multiple copies of meshes", default=True) 77 | fsim_multisim : BoolProperty(name="Simulate the multiple rigs", default=False) 78 | fsim_startangle : FloatProperty(name="Angle to Target", default=0.0) 79 | 80 | 81 | 82 | 83 | # fout = open(mypath) 84 | 85 | 86 | 87 | # bpy.types.Scene.FSimMainProps.fsim_start_frame = IntProperty(name="Simulation Start Frame", default=1, update=updateStartFrame) 88 | # bpy.types.Scene.FSimMainProps.fsim_end_frame = IntProperty(name="Simulation End Frame", default=250, update=updateEndFrame) 89 | # bpy.types.Scene.FSimMainProps.fsim_maxnum = IntProperty(name="Maximum number of copies", default=250) 90 | # bpy.types.Scene.FSimMainProps.fsim_copyrigs = BoolProperty(name="Distribute multiple copies of the rig", default=False) 91 | # bpy.types.Scene.FSimMainProps.fsim_copymesh = BoolProperty(name="Distribute multiple copies of meshes", default=False) 92 | # bpy.types.Scene.FSimMainProps.fsim_multisim = BoolProperty(name="Simulate the multiple rigs", default=False) 93 | # bpy.types.Scene.FSimMainProps.fsim_startangle = FloatProperty(name="Angle to Target", default=0.0) 94 | 95 | 96 | class ARMATURE_OT_FSim_Add(bpy.types.Operator): 97 | """Add a target object for the simulated fish to follow""" 98 | bl_label = "Add a target" 99 | bl_idname = "armature.fsim_add" 100 | bl_options = {'REGISTER', 'UNDO'} 101 | 102 | 103 | 104 | def execute(self, context): 105 | #Get the object 106 | TargetRig = context.active_object 107 | 108 | if TargetRig.type != "ARMATURE": 109 | print("Not an Armature", context.object.type) 110 | return {'CANCELLED'} 111 | 112 | TargetRoot = TargetRig.pose.bones.get("root") 113 | if (TargetRoot is None): 114 | print("No root bone in Armature") 115 | self.report({'ERROR'}, "No root bone in Armature - this addon needs a Rigify rig generated from a Shark Metarig") 116 | return {'CANCELLED'} 117 | 118 | TargetRoot["TargetProxy"] = TargetRig.name + '_proxy' 119 | #Add the proxy object 120 | bpy.ops.mesh.primitive_cube_add() 121 | bound_box = bpy.context.active_object 122 | #copy transforms 123 | bound_box.dimensions = TargetRig.dimensions 124 | bpy.ops.object.transform_apply(scale=True) 125 | bound_box.location = TargetRig.location 126 | bound_box.rotation_euler = TargetRig.rotation_euler 127 | bound_box.name = TargetRoot["TargetProxy"] 128 | bound_box.display_type = 'WIRE' 129 | bound_box.hide_render = True 130 | bound_box.visible_camera = False 131 | bound_box.visible_diffuse = False 132 | bound_box.visible_shadow = False 133 | bound_box["FSim"] = "FSim_"+TargetRig.name[:3] 134 | # if "FSim" in bound_box: 135 | # print("FSim Found") 136 | # bound_box.select = False 137 | #context.active_pose_bone = TargetRoot 138 | 139 | return {'FINISHED'} 140 | 141 | 142 | 143 | #UI Panels 144 | class AMATURE_MT_fsim_presets(Menu): 145 | bl_label = "FishSim Presets" 146 | preset_subdir = global_preset_subdir 147 | preset_operator = "script.execute_preset" 148 | COMPAT_ENGINES = {'BLENDER_RENDER', 'CYCLES_RENDER', 'BLENDER_GAME'} 149 | draw = Menu.draw_preset 150 | 151 | class AddPresetFSim(AddPresetBase, Operator): 152 | '''Add a Object Draw Preset''' 153 | bl_idname = "armature.addpresetfsim" 154 | bl_label = "Add FSim Draw Preset" 155 | preset_menu = "AMATURE_MT_fsim_presets" 156 | 157 | # variable used for all preset values 158 | preset_defines = [ 159 | "pFS = bpy.context.scene.FSimProps" 160 | ] 161 | 162 | # properties to store in the preset 163 | preset_values = [ 164 | "pFS.pMass", 165 | "pFS.pDrag", 166 | "pFS.pPower", 167 | "pFS.pMaxFreq", 168 | "pFS.pMaxTailAngle", 169 | "pFS.pAngularDrag", 170 | "pFS.pMaxSteeringAngle", 171 | "pFS.pTurnAssist", 172 | "pFS.pLeanIntoTurn", 173 | "pFS.pEffortGain", 174 | "pFS.pEffortIntegral", 175 | "pFS.pEffortRamp", 176 | "pFS.pMaxTailFinAngle", 177 | "pFS.pTailFinPhase", 178 | "pFS.pTailFinStiffness", 179 | "pFS.pTailFinStubRatio", 180 | "pFS.pMaxSideFinAngle", 181 | "pFS.pSideFinPhase", 182 | # "pFS.pSideFinStiffness", 183 | "pFS.pChestRatio", 184 | "pFS.pChestRaise", 185 | "pFS.pMaxVerticalAngle", 186 | "pFS.pRandom", 187 | "pFS.pMaxPecFreq", 188 | "pFS.pMaxPecAngle", 189 | "pFS.pPecPhase", 190 | "pFS.pPecStubRatio", 191 | "pFS.pPecStiffness", 192 | "pFS.pPecEffortGain", 193 | "pFS.pPecTurnAssist", 194 | "pFS.pHTransTime", 195 | "pFS.pSTransTime", 196 | "pFS.pPecOffset", 197 | "pFS.pHoverDist", 198 | "pFS.pHoverTailFrc", 199 | "pFS.pHoverMaxForce", 200 | "pFS.pHoverDerate", 201 | "pFS.pHoverTilt", 202 | "pFS.pPecDuration", 203 | "pFS.pPecDuty", 204 | "pFS.pHoverTwitch", 205 | "pFS.pHoverTwitchTime", 206 | "pFS.pPecSynch" 207 | "pFS.pPecTransition" 208 | ] 209 | 210 | # where to store the preset 211 | # preset_subdir = "../../extensions/user_default/FishSim/presets" 212 | 213 | 214 | class ARMATURE_OT_FSim_Run(bpy.types.Operator): 215 | """Simulate and add keyframes for the armature to make it swim towards the target""" 216 | bl_label = "Copy Models" 217 | bl_idname = "armature.fsim_run" 218 | bl_options = {'REGISTER', 'UNDO', 'PRESET'} 219 | 220 | # add_preset_files() 221 | 222 | root = None 223 | 224 | 225 | 226 | def CopyChildren(self, context, src_obj, new_obj): 227 | for childObj in src_obj.children: 228 | # print("Copying child: ", childObj.name) 229 | new_child = childObj.copy() 230 | new_child.data = childObj.data.copy() 231 | new_child.animation_data_clear() 232 | # new_child.location = childObj.location - src_obj.location 233 | new_child.parent = new_obj 234 | new_child.matrix_parent_inverse = childObj.matrix_parent_inverse 235 | context.collection.objects.link(new_child) 236 | new_child.select_set(True) 237 | for mod in new_child.modifiers: 238 | if mod.type == "ARMATURE": 239 | mod.object = new_obj 240 | 241 | 242 | def CopyRigs(self, context): 243 | # print("Populate") 244 | 245 | scene = context.scene 246 | src_obj = context.object 247 | if src_obj.type != 'ARMATURE': 248 | return {'CANCELLED'} 249 | src_obj.select_set(True) 250 | 251 | #make a list of armatures 252 | armatures = {} 253 | for obj in scene.objects: 254 | if obj.type == "ARMATURE" and obj.name[:3] == src_obj.name[:3]: 255 | root = obj.pose.bones.get("root") 256 | if root != None: 257 | if 'TargetProxy' in root: 258 | proxyName = root['TargetProxy'] 259 | if len(proxyName) > 1: 260 | armatures[proxyName] = obj.name 261 | 262 | #for each target... 263 | obj_count = 0 264 | for obj in scene.objects: 265 | if "FSim" in obj and (obj["FSim"][-3:] == src_obj.name[:3]): 266 | #Limit the maximum copy number 267 | if obj_count >= scene.FSimMainProps.fsim_maxnum: 268 | return {'FINISHED'} 269 | obj_count += 1 270 | 271 | #Go back to the first frame to make sure the rigs are placed correctly 272 | scene.frame_set(scene.FSimMainProps.fsim_start_frame) 273 | # scene.update() 274 | 275 | #if a rig hasn't already been paired with this target, and it's the right target type for this rig, then add a duplicated rig at this location if 'CopyRigs' is selected 276 | if (obj.name not in armatures) and (obj["FSim"][-3:] == src_obj.name[:3]): 277 | # print("time to duplicate") 278 | 279 | if scene.FSimMainProps.fsim_copyrigs: 280 | #If there is not already a matching armature, duplicate the template and update the link field 281 | new_obj = src_obj.copy() 282 | new_obj.data = src_obj.data.copy() 283 | # new_obj.animation_data_clear() 284 | context.collection.objects.link(new_obj) 285 | 286 | #Unlink from original action 287 | new_obj.animation_data.action = None 288 | 289 | #2.8 Issue Workout how to update drivers 290 | #Update drivers with new rig id 291 | for dr in new_obj.data.animation_data.drivers: 292 | for v1 in dr.driver.variables: 293 | # print("ID_name: ", v1.targets[0].id.name) 294 | # print("obj_name:", src_obj.name) 295 | if (v1.targets[0].id_type == 'OBJECT') and (v1.targets[0].id.name == src_obj.name): 296 | # print("Update_p", v1.targets[0].id) 297 | v1.targets[0].id = new_obj 298 | # print("Update", v1.targets[0].id) 299 | 300 | new_obj.location = obj.matrix_world.to_translation() 301 | new_obj.rotation_euler = obj.rotation_euler 302 | new_obj.rotation_euler.z += math.radians(scene.FSimMainProps.fsim_startangle) 303 | new_root = new_obj.pose.bones.get('root') 304 | new_root['TargetProxy'] = obj.name 305 | new_root.scale = (new_root.scale.x * obj.scale.x, new_root.scale.y * obj.scale.y, new_root.scale.z * obj.scale.z) 306 | context.view_layer.objects.active = new_obj 307 | new_obj.select_set(True) 308 | src_obj.select_set(False) 309 | 310 | #if 'CopyMesh' is selected duplicate the dependents and re-link 311 | if scene.FSimMainProps.fsim_copymesh: 312 | self.CopyChildren(context, src_obj, new_obj) 313 | 314 | #If there's already a matching rig, then just update it 315 | elif obj["FSim"][-3:] == src_obj.name[:3]: 316 | # print("matching armature", armatures[obj.name]) 317 | TargRig = scene.objects.get(armatures[obj.name]) 318 | if TargRig is not None: 319 | #reposition if required 320 | if scene.FSimMainProps.fsim_copyrigs: 321 | # TargRig.animation_data_clear() 322 | TargRig.location = obj.matrix_world.to_translation() 323 | TargRig.rotation_euler = obj.rotation_euler 324 | TargRig.rotation_euler.z += math.radians(scene.FSimMainProps.fsim_startangle) 325 | TargRig.keyframe_insert(data_path='rotation_euler', frame=(scene.FSimMainProps.fsim_start_frame)) 326 | TargRig.keyframe_insert(data_path='location', frame=(scene.FSimMainProps.fsim_start_frame)) 327 | 328 | #if no children, and the 'copymesh' flag set, then copy the associated meshes 329 | if scene.FSimMainProps.fsim_copymesh and len(TargRig.children) < 1: 330 | self.CopyChildren(context, src_obj, TargRig) 331 | 332 | #Leave the just generated objects selected 333 | # scene.objects.active = TargRig 334 | TargRig.select_set(True) 335 | src_obj.select_set(False) 336 | for childObj in TargRig.children: 337 | childObj.select_set(True) 338 | for childObj in src_obj.children: 339 | childObj.select_set(True) 340 | 341 | # #Animate 342 | # if scene.FSimMainProps.fsim_multisim and TargRig.name != src_obj.name: 343 | # # self.BoneMovement(TargRig, scene.FSimMainProps.fsim_start_frame, scene.FSimMainProps.fsim_end_frame, context) 344 | # bpy.ops.armature.fsimulate() 345 | 346 | 347 | 348 | 349 | 350 | def execute(self, context): 351 | #Get the object 352 | TargetRig = context.object 353 | scene = context.scene 354 | scene.FSimMainProps.fsim_targetrig = TargetRig.name 355 | scene = context.scene 356 | if TargetRig.type != "ARMATURE": 357 | print("Not an Armature", context.object.type) 358 | return {'FINISHED'} 359 | 360 | # print("Call test") 361 | # bpy.ops.armature.fsim_test() 362 | 363 | if scene.FSimMainProps.fsim_copyrigs or scene.FSimMainProps.fsim_copymesh: 364 | self.CopyRigs(context) 365 | # else: 366 | # # self.BoneMovement(TargetRig, scene.FSimMainProps.fsim_start_frame, scene.FSimMainProps.fsim_end_frame, context) 367 | # bpy.ops.armature.fsimulate() 368 | 369 | return {'FINISHED'} 370 | 371 | 372 | class ARMATURE_OT_AddFish(bpy.types.Operator): 373 | """Add a rigged fish to the scene""" 374 | bl_idname = "armature.addfish" 375 | bl_label = "Add Fish" 376 | 377 | FishSelector: bpy.props.EnumProperty( 378 | name="Options", 379 | description="Choose an option", 380 | items=[ 381 | # ('Cube', "Cube Collection", ""), 382 | ('BullShark', "Bull Shark", ""), 383 | ('Goldfish', "Goldfish", ""), 384 | ('ArcherFish', "Archer Fish", ""), 385 | ] 386 | ) 387 | 388 | def execute(self, context): 389 | filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Models/Models.blend") 390 | #directory = os.path.dirname(filepath) 391 | #filename = os.path.basename(filepath) 392 | # collectionname = "Cube" 393 | 394 | print("Filepath: ", filepath) 395 | 396 | # Construct the full path to the object 397 | #object_path = os.path.join(directory, filename, "Collection", collectionname) 398 | 399 | with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to): 400 | data_to.collections = [name for name in data_from.collections if name == self.FishSelector] 401 | 402 | print("data_to: ", data_to) 403 | 404 | for coll in data_to.collections: 405 | bpy.context.collection.children.link(coll) 406 | if coll.name == "BullShark": 407 | context.scene.FSimProps.pPower = 20.0 408 | context.scene.FSimProps.pTailFinStiffness = 0.7 409 | context.scene.FSimProps.pMaxSideFinAngle = 3.0 410 | bpy.data.texts["rig.shark_ui.py"].as_module() 411 | if coll.name == "Goldfish": 412 | context.scene.FSimProps.pPower = 1.0 413 | context.scene.FSimProps.pTailFinStiffness = 0.4 414 | context.scene.FSimProps.pMaxSideFinAngle = 15.0 415 | bpy.data.texts["rig.goldfish_ui.py"].as_module() 416 | if coll.name == "ArcherFish": 417 | context.scene.FSimProps.pPower = 3.0 418 | context.scene.FSimProps.pTailFinStiffness = 0.4 419 | context.scene.FSimProps.pMaxSideFinAngle = 15.0 420 | bpy.data.texts["rig.archer_ui.py"].as_module() 421 | 422 | # fix up link between armature and target if necessary 423 | ProxyName = "" 424 | RigRootBone = None 425 | for ob in coll.objects: 426 | if "proxy" in ob.name: 427 | ProxyName = ob.name 428 | if ob.type == 'ARMATURE': 429 | try: 430 | RigRootBone = ob.pose.bones['root'] 431 | ob.pose.bones["torso"]["neck_follow"] = 0.25 432 | ob.pose.bones["torso"]["head_follow"] = 0.25 433 | except: 434 | pass 435 | if ProxyName != "" and RigRootBone != None: 436 | RigRootBone["TargetProxy"] = ProxyName 437 | 438 | 439 | self.report({'INFO'}, f"Selected: {self.FishSelector}") 440 | 441 | return {'FINISHED'} 442 | 443 | def invoke(self, context, event): 444 | return context.window_manager.invoke_props_dialog(self) 445 | 446 | class ARMATURE_PT_FAdd(bpy.types.Panel): 447 | """Creates a Panel in the Object properties window""" 448 | bl_label = "FishAdd" 449 | bl_idname = "ARMATURE_PT_FAdd" 450 | bl_space_type = 'VIEW_3D' 451 | bl_region_type = 'UI' 452 | bl_category = "FishSim" 453 | #bl_context = "objectmode" 454 | 455 | 456 | 457 | @classmethod 458 | def poll(cls, context): 459 | return True 460 | # if context.object != None: 461 | # return (context.mode in {'OBJECT', 'POSE'}) and (context.object.type == "ARMATURE") 462 | # else: 463 | # return False 464 | 465 | def draw(self, context): 466 | layout = self.layout 467 | 468 | obj1 = context.object 469 | scene = context.scene 470 | 471 | layout.operator("armature.addfish") 472 | 473 | class ARMATURE_PT_FSim(bpy.types.Panel): 474 | """Creates a Panel in the Object properties window""" 475 | bl_label = "FishSim" 476 | bl_idname = "ARMATURE_PT_FSim" 477 | bl_space_type = 'VIEW_3D' 478 | bl_region_type = 'UI' 479 | bl_category = "FishSim" 480 | #bl_context = "objectmode" 481 | 482 | 483 | 484 | @classmethod 485 | def poll(cls, context): 486 | # return True 487 | if context.object != None: 488 | return (context.mode in {'OBJECT', 'POSE'}) and (context.object.type == "ARMATURE") 489 | else: 490 | return False 491 | 492 | def draw(self, context): 493 | layout = self.layout 494 | 495 | obj1 = context.object 496 | scene = context.scene 497 | 498 | #layout.operator("armature.addfish") 499 | layout.label(text="Simulation") 500 | # row = layout.row() 501 | layout.operator("armature.fsimulate") 502 | # row = layout.row() 503 | #layout.label(text="Animation Ranges") 504 | # row = layout.row() 505 | layout.prop(scene.FSimMainProps, "fsim_start_frame") 506 | # row = layout.row() 507 | layout.prop(scene.FSimMainProps, "fsim_end_frame") 508 | row = layout.row() 509 | layout.label(text="Add a Target to an existing rig") 510 | layout.operator("armature.fsim_add") 511 | # box = layout.box() 512 | # box.label(text="Multi Sim Options") 513 | layout.label(text="Copy Models to multiple targets") 514 | layout.operator("armature.fsim_run") 515 | layout.prop(scene.FSimMainProps, "fsim_copyrigs") 516 | layout.prop(scene.FSimMainProps, "fsim_copymesh") 517 | layout.prop(scene.FSimMainProps, "fsim_maxnum") 518 | layout.prop(scene.FSimMainProps, "fsim_startangle") 519 | 520 | class ARMATURE_PT_FSimPropPanel(bpy.types.Panel): 521 | """Creates a Panel in the Tool Panel""" 522 | bl_label = "Main Simulation Properties" 523 | bl_idname = "ARMATURE_PT_FSimPropPanel" 524 | bl_space_type = 'VIEW_3D' 525 | bl_region_type = 'UI' 526 | bl_category = "FishSim" 527 | bl_options = {'DEFAULT_CLOSED'} 528 | #bl_context = "objectmode" 529 | 530 | @classmethod 531 | def poll(cls, context): 532 | if context.object != None: 533 | return (context.mode in {'OBJECT', 'POSE'}) and (context.object.type == "ARMATURE") 534 | else: 535 | return False 536 | 537 | def draw(self, context): 538 | #Make sure the presets directory exists 539 | # add_preset_files() 540 | 541 | layout = self.layout 542 | 543 | scene = context.scene 544 | # row = layout.row() 545 | row = layout.row() 546 | row.menu("AMATURE_MT_fsim_presets", text=bpy.types.AMATURE_MT_fsim_presets.bl_label) 547 | # row.operator(AddPresetFSim.bl_idname, text="", icon='PLUS') 548 | # row.operator(AddPresetFSim.bl_idname, text="", icon='X').remove_active = True 549 | 550 | pFS = context.scene.FSimProps 551 | # row = layout.row() 552 | layout.label(text="Main Parameters") 553 | layout.prop(pFS, "pMass") 554 | layout.prop(pFS, "pDrag") 555 | layout.prop(pFS, "pPower") 556 | layout.prop(pFS, "pMaxFreq") 557 | layout.prop(pFS, "pMaxTailAngle") 558 | # row = layout.row() 559 | layout.label(text="Turning Parameters") 560 | layout.prop(pFS, "pAngularDrag") 561 | layout.prop(pFS, "pMaxSteeringAngle") 562 | layout.prop(pFS, "pTurnAssist") 563 | layout.prop(pFS, "pLeanIntoTurn") 564 | # row = layout.row() 565 | layout.label(text="Target Tracking") 566 | layout.prop(pFS, "pEffortGain") 567 | layout.prop(pFS, "pEffortIntegral") 568 | layout.prop(pFS, "pEffortRamp") 569 | # row = layout.row() 570 | layout.label(text="Fine Tuning") 571 | layout.prop(pFS, "pMaxTailFinAngle") 572 | layout.prop(pFS, "pTailFinPhase") 573 | layout.prop(pFS, "pTailFinStiffness") 574 | layout.prop(pFS, "pTailFinStubRatio") 575 | layout.prop(pFS, "pMaxSideFinAngle") 576 | layout.prop(pFS, "pSideFinPhase") 577 | layout.prop(pFS, "pChestRatio") 578 | layout.prop(pFS, "pChestRaise") 579 | layout.prop(pFS, "pMaxVerticalAngle") 580 | layout.prop(pFS, "pRandom") 581 | 582 | class ARMATURE_PT_FSimPecPanel(bpy.types.Panel): 583 | """Creates a Panel in the Tool Panel""" 584 | bl_label = "Pectoral Fin Properties" 585 | bl_idname = "ARMATURE_PT_FSimPecPanel" 586 | bl_space_type = 'VIEW_3D' 587 | bl_region_type = 'UI' 588 | bl_category = "FishSim" 589 | bl_options = {'DEFAULT_CLOSED'} 590 | #bl_context = "objectmode" 591 | 592 | @classmethod 593 | def poll(cls, context): 594 | if context.object != None: 595 | if (context.mode in {'OBJECT', 'POSE'}) and (context.object.type == "ARMATURE"): 596 | PecFinTopL = context.object.pose.bones.get("t_master.L") 597 | if PecFinTopL != None: 598 | return True 599 | return False 600 | 601 | def draw(self, context): 602 | #Make sure the presets directory exists 603 | # add_preset_files() 604 | 605 | layout = self.layout 606 | 607 | scene = context.scene 608 | # row = layout.row() 609 | # row.menu("AMATURE_MT_fsim_presets", text=bpy.types.AMATURE_MT_fsim_presets.bl_label) 610 | # row.operator(AddPresetFSim.bl_idname, text="", icon='ZOOMIN') 611 | # row.operator(AddPresetFSim.bl_idname, text="", icon='ZOOMOUT').remove_active = True 612 | 613 | pFS = context.scene.FSimProps 614 | layout.label(text="Main Pec Parameters") 615 | layout.prop(pFS, "pPecEffortGain") 616 | layout.prop(pFS, "pPecTurnAssist") 617 | layout.prop(pFS, "pMaxPecFreq") 618 | layout.prop(pFS, "pMaxPecAngle") 619 | layout.label(text="Pec Fin Tuning") 620 | layout.prop(pFS, "pPecPhase") 621 | layout.prop(pFS, "pPecStubRatio") 622 | layout.prop(pFS, "pPecStiffness") 623 | layout.prop(pFS, "pPecOffset") 624 | layout.prop(pFS, "pPecTransition") 625 | layout.label(text="Hover Mode Params") 626 | layout.prop(pFS, "pHoverDist") 627 | layout.prop(pFS, "pHTransTime") 628 | layout.prop(pFS, "pSTransTime") 629 | layout.prop(pFS, "pHoverTailFrc") 630 | layout.prop(pFS, "pHoverMaxForce") 631 | layout.prop(pFS, "pHoverDerate") 632 | layout.prop(pFS, "pHoverTilt") 633 | layout.label(text="Variation Tuning") 634 | layout.prop(pFS, "pPecDuration") 635 | layout.prop(pFS, "pPecDuty") 636 | layout.prop(pFS, "pHoverTwitch") 637 | layout.prop(pFS, "pHoverTwitchTime") 638 | layout.prop(pFS, "pPecSynch") 639 | 640 | #Register 641 | 642 | classes = ( 643 | FSimMainProps, 644 | ARMATURE_OT_FSim_Add, 645 | ARMATURE_PT_FAdd, 646 | ARMATURE_PT_FSim, 647 | ARMATURE_PT_FSimPropPanel, 648 | ARMATURE_PT_FSimPecPanel, 649 | AMATURE_MT_fsim_presets, 650 | AddPresetFSim, 651 | ARMATURE_OT_FSim_Run, 652 | ARMATURE_OT_AddFish, 653 | ) 654 | 655 | def register(): 656 | from bpy.utils import register_class 657 | 658 | # Classes. 659 | for cls in classes: 660 | register_class(cls) 661 | 662 | # bpy.utils.register_class(FSimMainProps) 663 | bpy.types.Scene.FSimMainProps = bpy.props.PointerProperty(type=FSimMainProps) 664 | # bpy.utils.register_class(ARMATURE_OT_FSim_Add) 665 | from . import FishSim 666 | FishSim.registerTypes() 667 | from . import metarig_menu 668 | metarig_menu.register() 669 | # bpy.utils.register_class(ARMATURE_PT_FSim) 670 | # bpy.utils.register_class(ARMATURE_PT_FSimPropPanel) 671 | # bpy.utils.register_class(ARMATURE_PT_FSimPecPanel) 672 | # bpy.utils.register_class(AMATURE_MT_fsim_presets) 673 | # bpy.utils.register_class(AddPresetFSim) 674 | # bpy.utils.register_class(ARMATURE_OT_FSim_Run) 675 | 676 | 677 | 678 | def unregister(): 679 | from bpy.utils import unregister_class 680 | del bpy.types.Scene.FSimMainProps 681 | # bpy.utils.unregister_class(FSimMainProps) 682 | # bpy.utils.unregister_class(ARMATURE_OT_FSim_Add) 683 | from . import metarig_menu 684 | metarig_menu.unregister() 685 | from . import FishSim 686 | FishSim.unregisterTypes() 687 | 688 | # Classes. 689 | for cls in classes: 690 | unregister_class(cls) 691 | 692 | # bpy.utils.unregister_class(ARMATURE_PT_FSim) 693 | # bpy.utils.unregister_class(ARMATURE_PT_FSimPropPanel) 694 | # bpy.utils.unregister_class(ARMATURE_PT_FSimPecPanel) 695 | # bpy.utils.unregister_class(AMATURE_MT_fsim_presets) 696 | # bpy.utils.unregister_class(AddPresetFSim) 697 | # bpy.utils.unregister_class(ARMATURE_OT_FSim_Run) 698 | 699 | 700 | if __name__ == "__main__": 701 | register() 702 | 703 | -------------------------------------------------------------------------------- /blender_manifest.toml: -------------------------------------------------------------------------------- 1 | schema_version = "1.0.0" 2 | 3 | id = "FishSim" 4 | version = "4.2.0" 5 | name = "FishSim" 6 | tagline = "Fish Swimming Simulation" 7 | description = "This addon makes it easy to animate the standard Rigify Shark armature along a custom path. Various fish models are also available on the linked Github page." 8 | type = "add-on" 9 | 10 | 11 | # List defined by Blender and server, see: 12 | # https://docs.blender.org/manual/en/4.2/extensions/tags.html 13 | tags = ["Animation", "Simulation"] 14 | 15 | blender_version_min = "4.2.0" 16 | 17 | maintainer = "Nerk