Merge pull request #1727 from MellowArpeggiation/master

BONGO BONGO BONGO I DON'T WANNA LEAVE THE CONGO
This commit is contained in:
HbmMods 2024-10-10 08:04:16 +02:00 committed by GitHub
commit ba638e947a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 2492 additions and 1608 deletions

View File

@ -93,6 +93,9 @@ If you want to make some changes to the mod, follow this guide:
* Click **Add Standard VM**; in the JRE home, navigate to the directory where the JDK is installed, then click finish and select it.
10. Code!
## Contributing animations
Weapon animations in NTM are stored in JSON files, which are used alongside OBJ models to produce high quality animations with reasonable filesizes. Import/Export Blender addons are available for versions 2.79, 3.2, and 4.0 in `tools`, and they should function reasonably well in newer versions as well. See the comments in the header of the export scripts for usage instructions.
## Compatibility notice
NTM has certain behaviors intended to fix vanilla code or to increase compatibility in certain cases where it otherwise would not be possible. These behaviors have the potential of not playing well with other mods, and while no such cases are currently known, here's a list of them.

Binary file not shown.

View File

@ -387,5 +387,9 @@ public class Orchestras {
if(type == AnimType.RELOAD || type == AnimType.RELOAD_CYCLE) {
if(timer == 0) player.worldObj.playSoundAtEntity(player, "hbm:weapon.glReload", 1F, 1F);
}
if(type == AnimType.INSPECT) {
if(timer == 9) player.worldObj.playSoundAtEntity(player, "hbm:weapon.glOpen", 1F, 1F);
if(timer == 27) player.worldObj.playSoundAtEntity(player, "hbm:weapon.glClose", 1F, 1F);
}
};
}

View File

@ -86,14 +86,13 @@ public class XFactory40mm {
@SuppressWarnings("incomplete-switch") public static BiFunction<ItemStack, AnimType, BusAnimation> LAMBDA_CONGOLAKE_ANIMS = (stack, type) -> {
int ammo = ((ItemGunBaseNT) stack.getItem()).getConfig(stack, 0).getReceivers(stack)[0].getMagazine(stack).getAmount(stack);
switch(type) {
case EQUIP: return new BusAnimation();
case EQUIP: return ResourceManager.congolake_anim.get("Equip");
case CYCLE: return ResourceManager.congolake_anim.get(ammo <= 1 ? "FireEmpty" : "Fire");
case RELOAD:
return ResourceManager.congolake_anim.get(ammo == 0 ? "ReloadEmpty": "ReloadStart");
case RELOAD: return ResourceManager.congolake_anim.get(ammo == 0 ? "ReloadEmpty": "ReloadStart");
case RELOAD_CYCLE: return ResourceManager.congolake_anim.get("Reload");
case RELOAD_END: return ResourceManager.congolake_anim.get("ReloadEnd");
case JAMMED: return new BusAnimation();
case INSPECT: return new BusAnimation();
case JAMMED: return ResourceManager.congolake_anim.get("Jammed");
case INSPECT: return ResourceManager.congolake_anim.get("Inspect");
}
return null;

View File

@ -109,6 +109,9 @@ public class BusAnimationKeyframe {
double change = value - previous.value;
double time = currentTime - startTime;
// Constant value optimisation
if(Math.abs(previous.value - value) < 0.000001) return value;
if(previous.interpolationType == IType.BEZIER) {
double v1x = startTime;
double v1y = previous.value;
@ -213,7 +216,13 @@ public class BusAnimationKeyframe {
}
private double sqrt3(double d) {
return Math.exp(Math.log(d) / 3.0);
if(d > 0.000001) {
return Math.exp(Math.log(d) / 3.0);
} else if(d > -0.000001) {
return 0;
} else {
return -Math.exp(Math.log(-d) / 3.0);
}
}
private double time(double start, double end, double duration) {
@ -235,12 +244,10 @@ public class BusAnimationKeyframe {
double q = (2 * a * a * a - a * b + c) / 2;
double d = q * q + p * p * p;
if(d > 0) {
if(d > 0.000001) {
double t = Math.sqrt(d);
return sqrt3(-q + t) + sqrt3(-q - t) - a;
}
if(d == 0) {
} else if(d > -0.000001) {
double t = sqrt3(-q);
double result = 2 * t - a;
if(result < 0.000001 || result > 1.000001) {
@ -277,9 +284,7 @@ public class BusAnimationKeyframe {
result = (-b + p) / (2 * a);
}
return result;
}
if(p == 0) {
} else if(p > -0.000001) {
return -b / (2 * a);
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -208,6 +208,8 @@
"weapon.coilgunShoot": {"category": "player", "sounds": [{"name": "weapon/coilgunShoot", "stream": false}]},
"weapon.glReload": {"category": "player", "sounds": [{"name": "weapon/glReload", "stream": false}]},
"weapon.glShoot": {"category": "player", "sounds": [{"name": "weapon/glShoot", "stream": false}]},
"weapon.glOpen": {"category": "player", "sounds": [{"name": "weapon/glOpen", "stream": false}]},
"weapon.glClose": {"category": "player", "sounds": [{"name": "weapon/glClose", "stream": false}]},
"weapon.44Shoot": {"category": "player", "sounds": [{"name": "weapon/44Shoot", "stream": false}]},
"weapon.trainImpact": {"category": "player", "sounds": [{"name": "weapon/trainImpact", "stream": false}]},
"weapon.nuclearExplosion": {"category": "player", "sounds": [{"name": "weapon/nuclearExplosion", "stream": true}]},

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,293 @@
# HOW TO USE
# Make sure all your animation actions start on frame 0 and are named as such:
# Name.Part
# and run the export, they'll be split into Animation groups with each part being assigned as a Bus.
# EG. Reload.Body will apply an animation called Reload to the bus called Body
# For best results, make sure your object Transform Mode is set to YZX Euler, so rotations match in-game
# When importing, you can use the Action Editor to assign the imported animations to parts to view and modify them
bl_info = {
"name": "Export JSON Animation",
"blender": (2, 79, 0),
"category": "Import-Export",
}
import bpy
import json
import math
import mathutils
from bpy_extras.io_utils import ExportHelper, ImportHelper
from bpy.props import StringProperty, BoolProperty, EnumProperty
from bpy.types import Operator
class ExportJSONAnimation(Operator, ExportHelper):
"""Exports an animation in a NTM JSON format"""
bl_idname = "export.ntm_json" # Unique identifier for buttons and menu items to reference.
bl_label = "Export NTM .json" # Display name in the interface.
bl_options = {'REGISTER'}
# ExportHelper mix-in class uses this.
filename_ext = ".json"
filter_glob = StringProperty(
default="*.json",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be clamped.
)
def execute(self, context): # execute() is called when running the operator.
print("writing JSON data to file...")
f = open(self.filepath, 'w', encoding='utf-8')
collection = {"anim": {}, "offset": {}, "hierarchy": {}}
dimensions = ["x", "z", "y"] # Swizzled to X, Z, Y
mult = [1, -1, 1] # +X, -Z, +Y
# Reset to first frame, so our offsets are set to model defaults
# If you get weird offset issues, make sure your model is in its rest pose before exporting
context.scene.frame_set(0)
animations = collection['anim']
offsets = collection['offset']
hierarchy = collection['hierarchy']
actions = bpy.data.actions
for action in actions:
split = action.name.split('.')
if len(split) != 2:
continue
name = split[0]
part = split[1]
if name not in animations:
animations[name] = {}
animations[name][part] = {}
animation = animations[name][part]
# Fetch all the animation data
for fcu in action.fcurves:
dimension = dimensions[fcu.array_index]
if not fcu.data_path in animation:
animation[fcu.data_path] = {}
if not dimension in animation[fcu.data_path]:
animation[fcu.data_path][dimension] = []
multiplier = mult[fcu.array_index]
if fcu.data_path == 'rotation_euler':
multiplier *= 180 / math.pi
previousMillis = 0
previousInterpolation = ""
for keyframe in fcu.keyframe_points:
timeToFrame = keyframe.co.x * (1 / context.scene.render.fps) * 1000
millis = timeToFrame - previousMillis
value = keyframe.co.y * multiplier
tuple = [value, millis, keyframe.interpolation, keyframe.easing]
if previousInterpolation == "BEZIER":
tuple.append(keyframe.handle_left.x * (1 / context.scene.render.fps) * 1000)
tuple.append(keyframe.handle_left.y * multiplier)
tuple.append(keyframe.handle_left_type)
if keyframe.interpolation == "BEZIER":
tuple.append(keyframe.handle_right.x * (1 / context.scene.render.fps) * 1000)
tuple.append(keyframe.handle_right.y * multiplier)
tuple.append(keyframe.handle_right_type)
if keyframe.interpolation == "ELASTIC":
tuple.append(keyframe.amplitude)
tuple.append(keyframe.period)
if keyframe.interpolation == "BACK":
tuple.append(keyframe.back)
previousMillis = timeToFrame
previousInterpolation = keyframe.interpolation
animation[fcu.data_path][dimension].append(tuple)
for object in bpy.data.objects:
if object.type != 'MESH':
continue
if object.parent:
hierarchy[object.name] = object.parent.name
if object.location == mathutils.Vector(): # don't export 0,0,0
continue
offsets[object.name] = [object.location.x, object.location.z, -object.location.y]
f.write(json.dumps(collection))
f.close()
return {'FINISHED'} # Lets Blender know the operator finished successfully.
class ImportJSONAnimation(Operator, ImportHelper):
"""Imports an animation from a NTM JSON format"""
bl_idname = "import.ntm_json" # important since its how bpy.ops.import_test.some_data is constructed
bl_label = "Import NTM .json"
bl_options = {'REGISTER'}
# ImportHelper mix-in class uses this.
filename_ext = ".json"
filter_glob = StringProperty(
default="*.json",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be clamped.
)
def execute(self, context):
print("reading JSON data from file...")
f = open(self.filepath, 'r', encoding='utf-8')
data = f.read()
f.close()
dimensions = ["x", "z", "y"] # Swizzled to X, Z, Y
mult = [1, -1, 1] # +X, -Z, +Y
collection = json.loads(data)
animations = collection["anim"]
for name in animations:
for part in animations[name]:
actionName = name + '.' + part
animation = animations[name][part]
action = bpy.data.actions.find(actionName) >= 0 and bpy.data.actions[actionName] or bpy.data.actions.new(actionName)
action.use_fake_user = True
# Keep the actions, in case they're already associated with objects
# but remove the frames to replace with fresh ones
for fcurve in action.fcurves:
action.fcurves.remove(fcurve)
# action.fcurves.clear()
for path in animation:
for dimension in animation[path]:
dimIndex = dimensions.index(dimension)
curve = action.fcurves.new(path, index=dimIndex)
multiplier = mult[dimIndex]
if path == 'rotation_euler':
multiplier *= math.pi / 180
millis = 0
previousInterpolation = ''
for tuple in animation[path][dimension]:
value = tuple[0] * multiplier
millis = millis + tuple[1]
frame = round(millis * context.scene.render.fps / 1000)
keyframe = curve.keyframe_points.insert(frame, value)
keyframe.interpolation = 'LINEAR'
if len(tuple) >= 3:
keyframe.interpolation = tuple[2]
if len(tuple) >= 4:
keyframe.easing = tuple[3]
i = 4
if previousInterpolation == 'BEZIER':
keyframe.handle_left.x = tuple[i] * context.scene.render.fps / 1000
i += 1
keyframe.handle_left.y = tuple[i] * multiplier
i += 1
keyframe.handle_left_type = tuple[i]
i += 1
if keyframe.interpolation == 'BEZIER':
keyframe.handle_right.x = tuple[i] * context.scene.render.fps / 1000
i += 1
keyframe.handle_right.y = tuple[i] * multiplier
i += 1
keyframe.handle_right_type = tuple[i]
i += 1
if keyframe.interpolation == 'ELASTIC':
keyframe.amplitude = tuple[i]
i += 1
keyframe.period = tuple[i]
i += 1
if keyframe.interpolation == 'BACK':
keyframe.back = tuple[i]
i += 1
previousInterpolation = keyframe.interpolation
for object in bpy.data.objects:
if object.type != 'MESH':
continue
bpy.ops.object.select_all(action='DESELECT')
object.select = True
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
object.rotation_mode = 'YZX'
if 'hierarchy' in collection:
hierarchy = collection["hierarchy"]
for name in hierarchy:
parent = hierarchy[name]
bpy.data.objects[name].parent = bpy.data.objects[parent]
offsets = collection["offset"]
for name in offsets:
offset = offsets[name]
for object in bpy.data.objects:
if object.type != 'MESH':
continue
bpy.ops.object.select_all(action='DESELECT')
object.select = True
if object.name == name:
savedLocation = bpy.context.scene.cursor_location
bpy.context.scene.cursor_location = (-offset[0], -offset[2], offset[1])
bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
bpy.context.scene.cursor_location = savedLocation
return {'FINISHED'}
def menu_export(self, context):
self.layout.operator(ExportJSONAnimation.bl_idname)
def menu_import(self, context):
self.layout.operator(ImportJSONAnimation.bl_idname)
def register():
bpy.utils.register_class(ExportJSONAnimation)
bpy.utils.register_class(ImportJSONAnimation)
if hasattr(bpy.types, "TOPBAR_MT_file_export"):
bpy.types.TOPBAR_MT_file_export.append(menu_export)
bpy.types.TOPBAR_MT_file_import.append(menu_import)
elif hasattr(bpy.types, "INFO_MT_file_export"):
bpy.types.INFO_MT_file_export.append(menu_export)
bpy.types.INFO_MT_file_import.append(menu_import)
def unregister():
bpy.utils.unregister_class(ExportJSONAnimation)
bpy.utils.unregister_class(ImportJSONAnimation)
if hasattr(bpy.types, "TOPBAR_MT_file_export"):
bpy.types.TOPBAR_MT_file_export.remove(menu_export)
bpy.types.TOPBAR_MT_file_import.remove(menu_import)
elif hasattr(bpy.types, "INFO_MT_file_export"):
bpy.types.INFO_MT_file_export.remove(menu_export)
bpy.types.INFO_MT_file_import.remove(menu_import)
# This allows you to run the script directly from Blender's Text editor
# to test the add-on without having to install it.
if __name__ == "__main__":
register()

View File

@ -0,0 +1,286 @@
# HOW TO USE
# Make sure all your animation actions start on frame 0 and are named as such:
# Name.Part
# and run the export, they'll be split into Animation groups with each part being assigned as a Bus.
# EG. Reload.Body will apply an animation called Reload to the bus called Body
# For best results, make sure your object Transform Mode is set to YZX Euler, so rotations match in-game
# When importing, you can use the Action Editor to assign the imported animations to parts to view and modify them
bl_info = {
"name": "Export JSON Animation",
"blender": (3, 2, 0),
"category": "Import-Export",
}
import bpy
import json
import math
import mathutils
from bpy_extras.io_utils import ExportHelper, ImportHelper
from bpy.props import StringProperty, BoolProperty, EnumProperty
from bpy.types import Operator
class ExportJSONAnimation(Operator, ExportHelper):
"""Exports an animation in a NTM JSON format"""
bl_idname = "export.ntm_json" # Unique identifier for buttons and menu items to reference.
bl_label = "Export NTM .json" # Display name in the interface.
bl_options = {'REGISTER'}
# ExportHelper mix-in class uses this.
filename_ext = ".json"
filter_glob: StringProperty(
default="*.json",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be clamped.
)
def execute(self, context): # execute() is called when running the operator.
print("writing JSON data to file...")
f = open(self.filepath, 'w', encoding='utf-8')
collection = {"anim": {}, "offset": {}, "hierarchy": {}}
dimensions = ["x", "z", "y"] # Swizzled to X, Z, Y
mult = [1, -1, 1] # +X, -Z, +Y
# Reset to first frame, so our offsets are set to model defaults
# If you get weird offset issues, make sure your model is in its rest pose before exporting
context.scene.frame_set(0)
animations = collection['anim']
offsets = collection['offset']
hierarchy = collection['hierarchy']
actions = bpy.data.actions
for action in actions:
split = action.name.split('.')
if len(split) != 2:
continue
name = split[0]
part = split[1]
if name not in animations:
animations[name] = {}
animations[name][part] = {}
animation = animations[name][part]
# Fetch all the animation data
for fcu in action.fcurves:
dimension = dimensions[fcu.array_index]
if not fcu.data_path in animation:
animation[fcu.data_path] = {}
if not dimension in animation[fcu.data_path]:
animation[fcu.data_path][dimension] = []
multiplier = mult[fcu.array_index]
if fcu.data_path == 'rotation_euler':
multiplier *= 180 / math.pi
previousMillis = 0
previousInterpolation = ""
for keyframe in fcu.keyframe_points:
timeToFrame = keyframe.co.x * (1 / context.scene.render.fps) * 1000
millis = timeToFrame - previousMillis
value = keyframe.co.y * multiplier
tuple = [value, millis, keyframe.interpolation, keyframe.easing]
if previousInterpolation == "BEZIER":
tuple.append(keyframe.handle_left.x * (1 / context.scene.render.fps) * 1000)
tuple.append(keyframe.handle_left.y * multiplier)
tuple.append(keyframe.handle_left_type)
if keyframe.interpolation == "BEZIER":
tuple.append(keyframe.handle_right.x * (1 / context.scene.render.fps) * 1000)
tuple.append(keyframe.handle_right.y * multiplier)
tuple.append(keyframe.handle_right_type)
if keyframe.interpolation == "ELASTIC":
tuple.append(keyframe.amplitude)
tuple.append(keyframe.period)
if keyframe.interpolation == "BACK":
tuple.append(keyframe.back)
previousMillis = timeToFrame
previousInterpolation = keyframe.interpolation
animation[fcu.data_path][dimension].append(tuple)
for object in bpy.data.objects:
if object.type != 'MESH':
continue
if object.parent:
hierarchy[object.name] = object.parent.name
if object.location == mathutils.Vector(): # don't export 0,0,0
continue
offsets[object.name] = [object.location.x, object.location.z, -object.location.y]
f.write(json.dumps(collection))
f.close()
return {'FINISHED'} # Lets Blender know the operator finished successfully.
class ImportJSONAnimation(Operator, ImportHelper):
"""Imports an animation from a NTM JSON format"""
bl_idname = "import.ntm_json" # important since its how bpy.ops.import_test.some_data is constructed
bl_label = "Import NTM .json"
bl_options = {'REGISTER'}
# ImportHelper mix-in class uses this.
filename_ext = ".json"
filter_glob: StringProperty(
default="*.json",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be clamped.
)
def execute(self, context):
print("reading JSON data from file...")
f = open(self.filepath, 'r', encoding='utf-8')
data = f.read()
f.close()
dimensions = ["x", "z", "y"] # Swizzled to X, Z, Y
mult = [1, -1, 1] # +X, -Z, +Y
collection = json.loads(data)
animations = collection["anim"]
for name in animations:
for part in animations[name]:
actionName = name + '.' + part
animation = animations[name][part]
action = bpy.data.actions.find(actionName) >= 0 and bpy.data.actions[actionName] or bpy.data.actions.new(actionName)
action.use_fake_user = True
# Keep the actions, in case they're already associated with objects
# but remove the frames to replace with fresh ones
for fcurve in action.fcurves:
action.fcurves.remove(fcurve)
# action.fcurves.clear()
for path in animation:
for dimension in animation[path]:
dimIndex = dimensions.index(dimension)
curve = action.fcurves.new(path, index=dimIndex)
multiplier = mult[dimIndex]
if path == 'rotation_euler':
multiplier *= math.pi / 180
millis = 0
previousInterpolation = ''
for tuple in animation[path][dimension]:
value = tuple[0] * multiplier
millis = millis + tuple[1]
frame = round(millis * context.scene.render.fps / 1000)
keyframe = curve.keyframe_points.insert(frame, value)
keyframe.interpolation = 'LINEAR'
if len(tuple) >= 3:
keyframe.interpolation = tuple[2]
if len(tuple) >= 4:
keyframe.easing = tuple[3]
i = 4
if previousInterpolation == 'BEZIER':
keyframe.handle_left.x = tuple[i] * context.scene.render.fps / 1000
i += 1
keyframe.handle_left.y = tuple[i] * multiplier
i += 1
keyframe.handle_left_type = tuple[i]
i += 1
if keyframe.interpolation == 'BEZIER':
keyframe.handle_right.x = tuple[i] * context.scene.render.fps / 1000
i += 1
keyframe.handle_right.y = tuple[i] * multiplier
i += 1
keyframe.handle_right_type = tuple[i]
i += 1
if keyframe.interpolation == 'ELASTIC':
keyframe.amplitude = tuple[i]
i += 1
keyframe.period = tuple[i]
i += 1
if keyframe.interpolation == 'BACK':
keyframe.back = tuple[i]
i += 1
previousInterpolation = keyframe.interpolation
for object in bpy.data.objects:
if object.type != 'MESH':
continue
bpy.ops.object.select_all(action='DESELECT')
object.select_set(True)
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False, properties=False)
object.rotation_mode = 'YZX'
if 'hierarchy' in collection:
hierarchy = collection["hierarchy"]
for name in hierarchy:
parent = hierarchy[name]
bpy.data.objects[name].parent = bpy.data.objects[parent]
offsets = collection["offset"]
for name in offsets:
offset = offsets[name]
for object in bpy.data.objects:
if object.type != 'MESH':
continue
bpy.ops.object.select_all(action='DESELECT')
object.select_set(True)
if object.name == name:
savedLocation = bpy.context.scene.cursor.location
bpy.context.scene.cursor.location = (-offset[0], -offset[2], offset[1])
bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
bpy.context.scene.cursor.location = savedLocation
return {'FINISHED'}
def menu_export(self, context):
self.layout.operator(ExportJSONAnimation.bl_idname)
def menu_import(self, context):
self.layout.operator(ImportJSONAnimation.bl_idname)
def register():
bpy.utils.register_class(ExportJSONAnimation)
bpy.utils.register_class(ImportJSONAnimation)
bpy.types.TOPBAR_MT_file_export.append(menu_export)
bpy.types.TOPBAR_MT_file_import.append(menu_import)
def unregister():
bpy.utils.unregister_class(ExportJSONAnimation)
bpy.utils.unregister_class(ImportJSONAnimation)
bpy.types.TOPBAR_MT_file_export.remove(menu_export)
bpy.types.TOPBAR_MT_file_import.remove(menu_import)
# This allows you to run the script directly from Blender's Text editor
# to test the add-on without having to install it.
if __name__ == "__main__":
register()

View File

@ -0,0 +1,283 @@
# HOW TO USE
# Make sure all your animation actions start on frame 0 and are named as such:
# Name.Part
# and run the export, they'll be split into Animation groups with each part being assigned as a Bus.
# EG. Reload.Body will apply an animation called Reload to the bus called Body
# For best results, make sure your object Transform Mode is set to YZX Euler, so rotations match in-game
# When importing, you can use the Action Editor to assign the imported animations to parts to view and modify them
bl_info = {
"name": "Export JSON Animation",
"blender": (4, 0, 0),
"category": "Import-Export",
}
import bpy
import json
import math
import mathutils
from bpy_extras.io_utils import ExportHelper, ImportHelper
from bpy.props import StringProperty, BoolProperty, EnumProperty
from bpy.types import Operator
class ExportJSONAnimation(Operator, ExportHelper):
"""Exports an animation in a NTM JSON format"""
bl_idname = "export.ntm_json" # Unique identifier for buttons and menu items to reference.
bl_label = "Export NTM .json" # Display name in the interface.
bl_options = {'REGISTER'}
# ExportHelper mix-in class uses this.
filename_ext = ".json"
filter_glob: StringProperty(
default="*.json",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be clamped.
)
def execute(self, context): # execute() is called when running the operator.
print("writing JSON data to file...")
f = open(self.filepath, 'w', encoding='utf-8')
collection = {"anim": {}, "offset": {}, "hierarchy": {}}
dimensions = ["x", "z", "y"] # Swizzled to X, Z, Y
mult = [1, -1, 1] # +X, -Z, +Y
# Reset to first frame, so our offsets are set to model defaults
# If you get weird offset issues, make sure your model is in its rest pose before exporting
context.scene.frame_set(0)
animations = collection['anim']
offsets = collection['offset']
hierarchy = collection['hierarchy']
actions = bpy.data.actions
for action in actions:
split = action.name.split('.')
if len(split) != 2:
continue
name = split[0]
part = split[1]
if name not in animations:
animations[name] = {}
animations[name][part] = {}
animation = animations[name][part]
# Fetch all the animation data
for fcu in action.fcurves:
dimension = dimensions[fcu.array_index]
if not fcu.data_path in animation:
animation[fcu.data_path] = {}
if not dimension in animation[fcu.data_path]:
animation[fcu.data_path][dimension] = []
multiplier = mult[fcu.array_index]
if fcu.data_path == 'rotation_euler':
multiplier *= 180 / math.pi
previousMillis = 0
previousInterpolation = ""
for keyframe in fcu.keyframe_points:
timeToFrame = keyframe.co.x * (1 / context.scene.render.fps) * 1000
millis = timeToFrame - previousMillis
value = keyframe.co.y * multiplier
tuple = [value, millis, keyframe.interpolation, keyframe.easing]
if previousInterpolation == "BEZIER":
tuple.append(keyframe.handle_left.x * (1 / context.scene.render.fps) * 1000)
tuple.append(keyframe.handle_left.y * multiplier)
tuple.append(keyframe.handle_left_type)
if keyframe.interpolation == "BEZIER":
tuple.append(keyframe.handle_right.x * (1 / context.scene.render.fps) * 1000)
tuple.append(keyframe.handle_right.y * multiplier)
tuple.append(keyframe.handle_right_type)
if keyframe.interpolation == "ELASTIC":
tuple.append(keyframe.amplitude)
tuple.append(keyframe.period)
if keyframe.interpolation == "BACK":
tuple.append(keyframe.back)
previousMillis = timeToFrame
previousInterpolation = keyframe.interpolation
animation[fcu.data_path][dimension].append(tuple)
for object in bpy.data.objects:
if object.type != 'MESH':
continue
if object.parent:
hierarchy[object.name] = object.parent.name
if object.location == mathutils.Vector(): # don't export 0,0,0
continue
offsets[object.name] = [object.location.x, object.location.z, -object.location.y]
f.write(json.dumps(collection))
f.close()
return {'FINISHED'} # Lets Blender know the operator finished successfully.
class ImportJSONAnimation(Operator, ImportHelper):
"""Imports an animation from a NTM JSON format"""
bl_idname = "import.ntm_json" # important since its how bpy.ops.import_test.some_data is constructed
bl_label = "Import NTM .json"
bl_options = {'REGISTER'}
# ImportHelper mix-in class uses this.
filename_ext = ".json"
filter_glob: StringProperty(
default="*.json",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be clamped.
)
def execute(self, context):
print("reading JSON data from file...")
f = open(self.filepath, 'r', encoding='utf-8')
data = f.read()
f.close()
dimensions = ["x", "z", "y"] # Swizzled to X, Z, Y
mult = [1, -1, 1] # +X, -Z, +Y
collection = json.loads(data)
animations = collection["anim"]
for name in animations:
for part in animations[name]:
actionName = name + '.' + part
animation = animations[name][part]
action = bpy.data.actions.find(actionName) >= 0 and bpy.data.actions[actionName] or bpy.data.actions.new(actionName)
action.use_fake_user = True
# Keep the actions, in case they're already associated with objects
# but remove the frames to replace with fresh ones
action.fcurves.clear()
for path in animation:
for dimension in animation[path]:
dimIndex = dimensions.index(dimension)
curve = action.fcurves.new(path, index=dimIndex)
multiplier = mult[dimIndex]
if path == 'rotation_euler':
multiplier *= math.pi / 180
millis = 0
previousInterpolation = ''
for tuple in animation[path][dimension]:
value = tuple[0] * multiplier
millis = millis + tuple[1]
frame = round(millis * context.scene.render.fps / 1000)
keyframe = curve.keyframe_points.insert(frame, value)
keyframe.interpolation = 'LINEAR'
if len(tuple) >= 3:
keyframe.interpolation = tuple[2]
if len(tuple) >= 4:
keyframe.easing = tuple[3]
i = 4
if previousInterpolation == 'BEZIER':
keyframe.handle_left.x = tuple[i] * context.scene.render.fps / 1000
i += 1
keyframe.handle_left.y = tuple[i] * multiplier
i += 1
keyframe.handle_left_type = tuple[i]
i += 1
if keyframe.interpolation == 'BEZIER':
keyframe.handle_right.x = tuple[i] * context.scene.render.fps / 1000
i += 1
keyframe.handle_right.y = tuple[i] * multiplier
i += 1
keyframe.handle_right_type = tuple[i]
i += 1
if keyframe.interpolation == 'ELASTIC':
keyframe.amplitude = tuple[i]
i += 1
keyframe.period = tuple[i]
i += 1
if keyframe.interpolation == 'BACK':
keyframe.back = tuple[i]
i += 1
previousInterpolation = keyframe.interpolation
for object in bpy.data.objects:
if object.type != 'MESH':
continue
bpy.ops.object.select_all(action='DESELECT')
object.select_set(True)
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False, properties=False)
object.rotation_mode = 'YZX'
if 'hierarchy' in collection:
hierarchy = collection["hierarchy"]
for name in hierarchy:
parent = hierarchy[name]
bpy.data.objects[name].parent = bpy.data.objects[parent]
offsets = collection["offset"]
for name in offsets:
offset = offsets[name]
for object in bpy.data.objects:
if object.type != 'MESH':
continue
bpy.ops.object.select_all(action='DESELECT')
object.select_set(True)
if object.name == name:
savedLocation = bpy.context.scene.cursor.location
bpy.context.scene.cursor.location = (-offset[0], -offset[2], offset[1])
bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
bpy.context.scene.cursor.location = savedLocation
return {'FINISHED'}
def menu_export(self, context):
self.layout.operator(ExportJSONAnimation.bl_idname)
def menu_import(self, context):
self.layout.operator(ImportJSONAnimation.bl_idname)
def register():
bpy.utils.register_class(ExportJSONAnimation)
bpy.utils.register_class(ImportJSONAnimation)
bpy.types.TOPBAR_MT_file_export.append(menu_export)
bpy.types.TOPBAR_MT_file_import.append(menu_import)
def unregister():
bpy.utils.unregister_class(ExportJSONAnimation)
bpy.utils.unregister_class(ImportJSONAnimation)
bpy.types.TOPBAR_MT_file_export.remove(menu_export)
bpy.types.TOPBAR_MT_file_import.remove(menu_import)
# This allows you to run the script directly from Blender's Text editor
# to test the add-on without having to install it.
if __name__ == "__main__":
register()