Skip to content

Instantly share code, notes, and snippets.

@SvizelPritula
Last active April 17, 2020 15:54
Show Gist options
  • Save SvizelPritula/990579d7b3fc8f2ec4899cf70dd44598 to your computer and use it in GitHub Desktop.
Save SvizelPritula/990579d7b3fc8f2ec4899cf70dd44598 to your computer and use it in GitHub Desktop.
How to use:
1. Create your animation in Blender. Use 1x1x1 cubes. (That's the default cube with a scale of 0.5) Use 20 fps. Only location and rotation is relevant.
2. Import the export.py script, and pick a location to save the data. Input it into the path variable.
3. Select the blocks you want exported and run the script.
4. Make an empty datapack inside your world folder. Create your pack.mcmeta.
5. Install node.js and download the generateDataPack.js script.
6. Fill in all the paths. That's a path to the root datapack folder, a path to the animation data from step 2.
7. Put in the coordinates. That is the position in your world that corresponds to 0 0 0 in Blender. The mapping of Blender to Minecraft coordinates is X=Z, Y=X, Z=Y
8. Fill in the namespace and name. The namespace is the regular Minecraft namespace, the name is a unique name within the namespace.
9. Fill in the scoreboard objective. Create it in Minecraft with the dummy type.
10. Fill in the materials in the getMaterial function. The current function takes the part of the obeject name before the first dot and translates that according to the table. If you know JS, you can change the function.
11. Run the script. This will create the datapack.
12. Create the resource pack. Create the pack.mcmeta.
13. For every material used, create a file named assets\minecraft\models\item\[block].json and copy the model.json file into it. Replace stone with the correct material.
14. Reload your resource and datapack.
15. Run /function [namespace]:[name]/spawn to spawn the structure and /function [namespace]:[name]/start to initialize the scoreboards
16. To play the animation, run /function [namespace]:[name]/advance 20 times per second, either using a repeating command block or #minecraft:tick
17. To rewind to the start, use /function [namespace]:[name]/start again
18. To delete the structure, use /function [namespace]:[name]/kill
import bpy
import json
from math import *
from mathutils import *
# Select objects to export and run script
path = "" # Where to save animation data (JSON)
frames = []
scene = bpy.context.scene
objects = bpy.context.selected_objects
for frame in range(scene.frame_start, scene.frame_end+1, scene.frame_step):
scene.frame_set(frame)
frames.append({})
for object in objects:
# Convert everything to Minecrafts system
rotation = Euler((0,0,0), "YZX")
rotation.rotate(object.rotation_euler)
x, y, z = object.location
rx, ry, rz = rotation
frames[-1][object.name] = {
"position": [y, z, x],
"rotation": [degrees(ry), -degrees(rz), -degrees(rx)]
}
data = json.dumps({
"frameData": {
"start": scene.frame_start,
"end": scene.frame_end,
"step": scene.frame_step
},
"objects": [object.name for object in objects],
"frames": frames
})
with open(path, "w") as file:
file.write(data)
file.close()
const fs = require('fs');
const path = require('path');
const animationPath = ""; // Should point to the data exported from Blender
const datapackDir = ""; // Points to the root directory of the datapack. Should contain pack.mcmeta
const namespace = "anim"; // Datapack namespace. Must be unique within world
const animationName = "example"; // Name of animation. Must be unique within namespace
const origin = [-32, 100, 64]; // What coordinates 0 0 0 in blender correspond to (Minecraft coordiantes)
const scoreboardObjective = "anim.frame"; // The scoreboard objective that stores the current frame. Present on all armor stands
function getMaterial(name) { // Governs what material gets assigned to what block
var type = name.split('.')[0]; // Change as needed
return {
"Block": "minecraft:stone",
"Action Block": "minecraft:iron_block"
}[type] || "minecraft:stone";
}
var animation = JSON.parse(fs.readFileSync(animationPath, { encoding: "utf-8" }));
var { frameData, objects, frames } = animation;
objects.sort();
var functionDir = path.join(datapackDir, "data", namespace, "functions", animationName);
function translatePosition([x, y, z]) { // Adds the origin to coordinates, so the animation does not play at 0 0 0.
var [ox, oy, oz] = origin;
return [x + ox, y - 1.4375 + oy, z + oz]; // Also offsets armor stand height
}
function translateRotation([x, y, z]) { // Identity for now
return [x, y, z];
}
function generateTag(components) { // Creates tag by joining array with dots and removing disallowed characters
return components.map(component => (
component.replace(/[^-_a-zA-Z0-9]/g, "")
)).join('.');
}
function generateSpecificTag(name) { // Generates tag that identifies a specific armor stand
return generateTag([namespace, animationName, name]);
}
function generateGenericTag() { // Generates tag all armor stands for this animation share
return generateTag([namespace, animationName]);
}
function forceDecimalPoint(n) { // Converts number to string with at least one digit after the decimal point. Used in the summon command, since Minecraft assumes 1 == 1.5
return n % 1 == 0 ? n.toFixed(1) : n.toString();
}
function generateSummon(name, position, rotation) { // Generates command within the spawn function that summons the armor stand in the position for the first frame
var [x, y, z] = translatePosition(position);
var [rx, ry, rz] = translateRotation(rotation);
var tags = [generateSpecificTag(name), generateGenericTag()];
var material = getMaterial(name);
var coordinates = `${forceDecimalPoint(x)} ${forceDecimalPoint(y)} ${forceDecimalPoint(z)}`;
var nbtStatic = "NoGravity:1b,Invulnerable:1b,Invisible:1b,DisabledSlots:4144959";
var nbtArmorItems = `ArmorItems:[{},{},{},{id:"${material}",Count:1b}]`;
var nbtTags = `Tags:[${tags.map(t => `"${t}"`).join(',')}]`;
var nbtPose = `Pose:{Head:[${rx}f,${ry}f,${rz}f]}`;
return `summon minecraft:armor_stand ${coordinates} {${nbtTags},${nbtPose},${nbtArmorItems},${nbtStatic}}`;
} // summon minecraft:armor_stand 10.5 14.0625 20.5 {Tags:["anim.example.Block000","anim.example"],Pose:{Head:[0f,90f,0f]},ArmorItems:[{},{},{},{id:"minecraft:stone",Count:1b}],NoGravity:1b,Invulnerable:1b,Invisible:1b,DisabledSlots:4144959}
function generateData(name, position, rotation) { // Generates command within the frame/n function that sets the position and pose based on the tag
var [x, y, z] = translatePosition(position);
var [rx, ry, rz] = translateRotation(rotation);
var tag = generateSpecificTag(name);
var selector = `@s[tag=${tag}]`;
var nbtPos = `Pos:[${x}d,${y}d,${z}d]`;
var nbtPose = `Pose:{Head:[${rx}f,${ry}f,${rz}f]}`;
return `data merge entity ${selector} {${nbtPos},${nbtPose}}`;
} // data merge entity @s[tag=anim.example.Block000] {Pos:[$10.5d,14.0625d,20.5d],Pose:{Head:[0f,90f,0f]}}
function generateFunction(frame) { // Generates commands within the update function that call the correct frame/n function
var tag = generateGenericTag();
var selector = `@e[tag=${tag},scores={${scoreboardObjective}=${frame}}]`;
var func = `${namespace}:${animationName}/frame/${frame}`
return `execute as ${selector} run function ${func}`;
} // execute as @e[tag=anim.example,scores={anim.frame=1] run function anim:example/frame/1
function createDirectory() { // Creates directories within datapack
try {
fs.mkdirSync(functionDir, { recursive: true });
fs.mkdirSync(path.join(functionDir, "frame"));
} catch { }
}
function createSpawn() { // Creates function that spawns all armor stands in position for first frame
var file = fs.createWriteStream(path.join(functionDir, "spawn.mcfunction"));
for (var object of objects) {
var { position, rotation } = frames[0][object];
file.write(generateSummon(object, position, rotation));
file.write('\n');
}
file.end();
}
function createUpdate() { // Creates function that updates position of all the armor stands
var updateFile = fs.createWriteStream(path.join(functionDir, "update.mcfunction"));
// The update function checks the frame score and calls the corresponding function in the frame directory
// Having all commands in one function lags too much
for (var i = 0; i < frames.length; i++) {
var frame = frameData.start + i * frameData.step;
updateFile.write(generateFunction(frame));
updateFile.write('\n');
var frameFile = fs.createWriteStream(path.join(functionDir, 'frame', `${frame}.mcfunction`));
// The frame/n function updates it's caller to the position and rotation for frame n
for (var object of objects) {
var { position, rotation } = frames[i][object];
frameFile.write(generateData(object, position, rotation));
frameFile.write('\n');
}
frameFile.end();
}
updateFile.end();
}
function createKill() { // Creates function that removes all armor stands for this animation
var file = fs.createWriteStream(path.join(functionDir, "kill.mcfunction"));
file.write(`kill @e[tag=${generateGenericTag()}]`);
file.end();
}
function createStart() { // Creates function that resets all blocks to first frame and calls update
var file = fs.createWriteStream(path.join(functionDir, "start.mcfunction"));
file.write(`scoreboard players set @e[tag=${generateGenericTag()}] ${scoreboardObjective} ${frameData.start}`);
file.write('\n');
file.write(`function ${namespace}:${animationName}/update`);
file.end();
}
function createAdvance() { // Creates function that advances all blocks to next frame and calls update
var file = fs.createWriteStream(path.join(functionDir, "advance.mcfunction"));
file.write(`scoreboard players add @e[tag=${generateGenericTag()}] ${scoreboardObjective} ${frameData.step}`);
file.write('\n');
file.write(`function ${namespace}:${animationName}/update`);
file.end();
}
createDirectory();
createSpawn();
createUpdate();
createStart();
createAdvance();
createKill();
{
"_comment": "Use as the item/stone.json model and other needed blocks (change parent)",
"parent": "block/stone",
"display": {
"head": {
"rotation": [ 0, 0, 0 ],
"translation": [ 0, -6.4, 0 ],
"scale": [ 1.6, 1.6, 1.6 ],
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment