├── 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 | ![image](https://user-images.githubusercontent.com/73910894/202816365-5e2820d9-0bf1-4ab9-a2a0-d6cb478faf2f.png) 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 | --------------------------------------------------------------------------------