mirror of
https://github.com/HbmMods/Hbm-s-Nuclear-Tech-GIT.git
synced 2026-01-25 10:32:49 +00:00
Merge pull request #1727 from MellowArpeggiation/master
BONGO BONGO BONGO I DON'T WANNA LEAVE THE CONGO
This commit is contained in:
commit
ba638e947a
@ -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.
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
@ -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}]},
|
||||
|
||||
BIN
src/main/resources/assets/hbm/sounds/weapon/glClose.ogg
Normal file
BIN
src/main/resources/assets/hbm/sounds/weapon/glClose.ogg
Normal file
Binary file not shown.
BIN
src/main/resources/assets/hbm/sounds/weapon/glOpen.ogg
Normal file
BIN
src/main/resources/assets/hbm/sounds/weapon/glOpen.ogg
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 14 KiB |
293
tools/export-json-animation-2_79.py
Normal file
293
tools/export-json-animation-2_79.py
Normal 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()
|
||||
286
tools/export-json-animation-3_2.py
Normal file
286
tools/export-json-animation-3_2.py
Normal 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()
|
||||
283
tools/export-json-animation-4_0.py
Normal file
283
tools/export-json-animation-4_0.py
Normal 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()
|
||||
Loading…
x
Reference in New Issue
Block a user