├── DatapackTemplate
├── data
│ ├── cb
│ │ └── functions
│ │ │ └── AddYourFunctionsHere.mcfunction
│ └── minecraft
│ │ └── tags
│ │ └── functions
│ │ ├── load.json
│ │ └── tick.json
└── pack.mcmeta
├── DatapackExample.zip
├── README.md
└── main.py
/DatapackTemplate/data/cb/functions/AddYourFunctionsHere.mcfunction:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/DatapackExample.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rezzorex/ChiseledBookshelfAnimations/HEAD/DatapackExample.zip
--------------------------------------------------------------------------------
/DatapackTemplate/data/minecraft/tags/functions/load.json:
--------------------------------------------------------------------------------
1 | {
2 | "values": [
3 | "cb:load"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/DatapackTemplate/data/minecraft/tags/functions/tick.json:
--------------------------------------------------------------------------------
1 | {
2 | "values": [
3 | "cb:tick"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/DatapackTemplate/pack.mcmeta:
--------------------------------------------------------------------------------
1 | {
2 | "pack": {
3 | "pack_format": 10,
4 | "description": "Convert videos to bookshelf animation."
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chiseled bookshelf animations
2 | A fun little project to generate Minecraft pixel art animations using the new block "chiseled bookshelf" and data packs. Check out the demo [here](https://www.reddit.com/user/Rezzorex/comments/yysbsb/bad_apple_recreated_with_minecraft_chiseled/).
3 |
4 | 
5 |
6 | ## Required python packages
7 |
8 | [ffmpeg-python](https://pypi.org/project/ffmpeg-python/)
9 | >pip install ffmpeg-python
10 |
11 | [opencv-python](https://pypi.org/project/opencv-python/)
12 | >pip install opencv-python
13 |
14 | [numpy](https://pypi.org/project/numpy/)
15 | >pip install numpy
16 |
17 | ## Usage instructions
18 | 1. Make sure you have the latest version of [python](https://www.python.org/downloads/) installed.
19 | 2. Open a command Window by pressing ⊞ Win + r and then enter "cmd" and hit Enter. Install all of the required python packages by pasting each of the commands from above.
20 | 3. Download the [code](https://github.com/Rezzorex/ChiseledBookshelfAnimations/archive/refs/heads/main.zip) and extract the files into a folder. Add the video you want to convert to that same folder.
21 | 4. Open the "main.py" file and a window should pop up. Follow the instructions on screen and then wait until the program has finished. There should now be a new folder called "output".
22 | 5. Copy the files from the output folder into your data packs "functions" folder. If you don't already have a data pack you can use the data pack template available with the download. If you do simply paste the files into "data/cb/functions".
23 | 6. Create a Minecraft world with the "update_1_20" data pack enabled. (You need to do this before generating the world, it can't be done after)
24 | 7. Add the generated data pack to your world and it should appear at the cords you specified.
25 |
26 | To restart the animation you can run the command "/scoreboard player set .current frame 0"
27 |
28 | ## Note:
29 | The data packs generated currently only works on snapshot 22w46a or later with the "update_1_20" data pack enabled.
30 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import math
2 | import ffmpeg
3 | import cv2
4 | import numpy as np
5 | import os
6 |
7 |
8 | def main():
9 | print("Add a video to the same folder as this file and then input the filename below. (e.g. \"BadApple.mp4\")")
10 | filepath = input("> ")
11 | print("\nInput your desired height in pixels.")
12 | height = int(input("> ")) * 2 + 2
13 | print("\nInput the duration if you want to trim the video, otherwise leave it blank.")
14 | duration = input("> ")
15 | print("\nInput the start coordinates of the canvas.")
16 | startX = int(input("X: > "))
17 | startY = int(input("Y: > "))
18 | startZ = int(input("Z: > "))
19 | print("Converting video...")
20 |
21 | stream = ffmpeg.input(filepath)
22 |
23 | if duration:
24 | stream = ffmpeg.trim(stream, duration=duration)
25 |
26 | if os.path.exists("output.mp4"):
27 | os.remove("output.mp4")
28 |
29 | stream = ffmpeg.filter(stream, 'scale', width=height, height=-2)
30 | stream = ffmpeg.filter(stream, 'fps', fps=20, round='up')
31 | stream = ffmpeg.output(stream, 'output.mp4')
32 | ffmpeg.run(stream)
33 |
34 | print("Video converted successfully, generating animation frames, this may take a while.")
35 |
36 | stream = cv2.VideoCapture('output.mp4')
37 | frame_count = stream.get(cv2.CAP_PROP_FRAME_COUNT)
38 | count = 0
39 | success = 1
40 |
41 | if not os.path.exists("output"):
42 | os.makedirs("output")
43 |
44 | if not os.path.exists("output/frames"):
45 | os.makedirs("output/frames")
46 |
47 | tick = open('output/tick.mcfunction', 'a')
48 | tick.write("execute if score .current frame <= .max frame run scoreboard players add .current frame 1\nexecute if score .current frame > .max frame run scoreboard players set .current frame 0\n")
49 | tick.close()
50 |
51 | currentPlacement = dict()
52 |
53 | while success:
54 | success, image = stream.read()
55 | image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
56 | ret, thresh_hold = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY_INV)
57 |
58 | blocks = dict()
59 |
60 | file = open('output/frames/frame' + str(count) + '.mcfunction', 'a')
61 |
62 | tick = open('output/tick.mcfunction', 'a')
63 | tick.write("execute if score .current frame matches " + str(count) + " run function cb:frames/frame" + str(count) + "\n")
64 | tick.close()
65 |
66 | load = open('output/load.mcfunction', 'w')
67 | load.write("scoreboard objectives add frame dummy\n\nscoreboard players set .current frame 0\nscoreboard players set .max frame " + str(count))
68 | load.close()
69 |
70 | for (x, y), pixel in np.ndenumerate(thresh_hold):
71 | currentBlock = str(math.ceil(math.floor(x + 1) / 3)) + '-' + str(math.ceil(math.floor(y + 1) / 2))
72 |
73 | if not currentBlock in blocks:
74 | blocks[currentBlock] = []
75 |
76 | if pixel > 255 / 2:
77 | blocks.get(currentBlock).append("true")
78 | else:
79 | blocks.get(currentBlock).append("false")
80 |
81 | if len(blocks.get(currentBlock)) == 6:
82 | command = "setblock " + str(startX + math.ceil(math.floor(y) / 3)) + " " + str(startY - math.ceil(math.floor(x) / 3)) + " " + str(startZ) + " minecraft:chiseled_bookshelf["
83 | nbtData = ""
84 | i = 0
85 |
86 | for line in blocks[currentBlock]:
87 | if line == "true":
88 | nbtData = nbtData + "slot_" + str(i) + "_occupied=" + line + ","
89 |
90 | i += 1
91 |
92 | if count == 0 or currentPlacement.get(currentBlock) != nbtData:
93 | file.write('# ' + currentBlock + ':\n' + command + nbtData + ']\n\n')
94 | currentPlacement[currentBlock] = nbtData
95 |
96 | file.close()
97 | count += 1
98 |
99 | print(str(count) + "/" + str(int(frame_count)) + " frames processed.")
100 |
101 | if count >= frame_count:
102 | success = 0
103 |
104 | print("Done, files saved to the \"output\" folder. Move everything into the \"functions\" folder of your datapack!")
105 | input("Hit enter to close.")
106 |
107 |
108 | if __name__ == '__main__':
109 | main()
110 |
--------------------------------------------------------------------------------