└── addons └── blenderspritesheetimporter ├── LICENSE ├── README.md ├── blender ├── CharacterTemplate.blend └── render_8_direction_sprites.py ├── blenderspritesheetimporter.gd ├── icon.png ├── jbss_importer.gd ├── plugin.cfg └── tutorial ├── .gdignore ├── animation_import.jpg ├── enable_add_on.jpg ├── example.gif ├── example_scene.jpg ├── export_to_mixamo.jpg ├── fixing_walk_animation_1.jpg ├── fixing_walk_animation_2.jpg ├── import_plugin.jpg ├── importing_animations_in_godot.jpg ├── install_plugin.jpg ├── mixamo_export_settings.jpg ├── node_hierarchy.jpg ├── preparing_for_export_1.jpg ├── preparing_for_export_2.jpg ├── preparing_for_export_3.jpg ├── preparing_for_export_4.jpg ├── preparing_for_export_5.jpg ├── preparing_for_export_6.jpg └── run_the_script.jpg /addons/blenderspritesheetimporter/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 OddMode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | MIT License 25 | 26 | Copyright (c) 2022 Joe 27 | 28 | Permission is hereby granted, free of charge, to any person obtaining a copy 29 | of this software and associated documentation files (the "Software"), to deal 30 | in the Software without restriction, including without limitation the rights 31 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 32 | copies of the Software, and to permit persons to whom the Software is 33 | furnished to do so, subject to the following conditions: 34 | 35 | The above copyright notice and this permission notice shall be included in all 36 | copies or substantial portions of the Software. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 44 | SOFTWARE. 45 | -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/README.md: -------------------------------------------------------------------------------- 1 | # Blender SpriteSheet Importer 2 | 3 | While looking for a streamlined way to export 8-way direction characters to use in Godot, I came across [this YouTube video](https://www.youtube.com/watch?v=l1Io7fLYV4o) that provided a nice setup for my experiments. 4 | I started working with it, and was satisfied with the results except importing the generated spritesheets in Godot required a lot of manual work. 5 | To automate this process, I created an import add-on that hopefully simplifies importing these animations. 6 | I made up a JSON format representing the resulting animations that can be directly imported in Godot as a `SpriteFrames` resource to be used in an `AnimatedSprite2D` node. 7 | 8 | As a side note, kudos to Godot developers for the editor plugin architecture - it's very powerful and only took me an hour to get this code working by following the [step by step tutorial](https://docs.godotengine.org/en/stable/tutorials/plugins/editor/making_plugins.html). 9 | 10 | ## Tutorial 11 | 12 | Here's a quick tutorial on how to use this plugin, importing a Mixamo animated model in Blender to generate 8-way 2D animations ready for use in Godot. 13 | 14 | ### Plugin installation 15 | 16 | First and foremost, you have to import the plugin from the AssetLib. 17 | Currently the plugin is not yet part of the Asset store, so you have to download the [zip file](https://github.com/oddmode-studio/BlenderSpriteSheetImporter/releases/latest) and import it manually: 18 | 19 | ![Plugin installation](./tutorial/import_plugin.jpg) 20 | 21 | ![Plugin installation](./tutorial/install_plugin.jpg) 22 | 23 | After a successful installation you can delete the `tutorial` folder, but keep the `blender` folder as it contains both the scene template and the Python script to export the spritesheets. 24 | 25 | Enable the plugin in the Project Settings: 26 | 27 | ![Plugin activation](./tutorial/enable_add_on.jpg) 28 | 29 | Now Godot supports opening files with `.JBSS` extension as `SpriteFrames` objects. How do you create this kind of files? Let's jump to Blender! 30 | 31 | ### Create the model and the animations 32 | 33 | The requirement to generate the spritesheets is obviously having a model to animate or, in case you lack the artistic skills like I do, finding one on the internet. 34 | In this tutorial I use this [Monk Character](https://sketchfab.com/3d-models/monk-character-8cacbd85a5b84f59a8c9000d7a6dcca2) from SketchFab (thanks [Inuciian](https://sketchfab.com/Inuciian) for your model and the permissive license). 35 | 36 | I used Mixamo to apply some animations on it, so to import the model there I first had to open it in Blender and export it to .FBX format. 37 | Once the model is exported, go to [Mixamo](www.mixamo.com), select the animation you prefer (I chose Lite Magic Pack, which holds 14 different animations, but I will only export 3 of them in this tutorial), click on "Upload character" and drag your exported FBX file. 38 | When Mixamo completes its magic, you can preview the animations and if you're satisfied with them you can hit "Download". 39 | 40 | I used these setting for download: ![Mixamo export settings](./tutorial/mixamo_export_settings.jpg) 41 | This will give you a ZIP file with 14 FBX files containing the animations, plus the original FBX model you uploaded earlier. 42 | 43 | ### Blender setup 44 | 45 | Copy the files from the `addons/blenderspritesheetimporter/blender` folder somewhere handy (I use the `assets/raw/` folder of the Godot project so I can keep it alongside the resulting spritesheets). 46 | 47 | > Please note that the next steps are similar to the ones described in the above-mentioned [video](https://www.youtube.com/watch?v=l1Io7fLYV4o). If something is not clear in this tutorial, you may refer to the video for the Blender scene setup. 48 | 49 | Open Blender (I used 4.0.2, but the workflow should be similar in other versions as well), remove the default cube and do `File -> Import -> FBX` to import the files you downloaded from Mixamo. 50 | I imported the animations "standing idle.fbx", "Standing Walk Forward.fbx" and "Standing 1H Magic Attack 01.fbx". 51 | 52 | They will appear in the Outline view as `Armature_Monk.xxx` nodes: 53 | 54 | ![](./tutorial/animation_import.jpg) 55 | 56 | You will have to identify which animation is stored in each node and provide a meaningful name so you can distinguish them later, as I did for `attack` in the above picture. 57 | 58 | For the `walk` animation, the armature is moving forward (there is no way to chose to animate "in place" from Mixamo for this animations pack) but we want everything to happen on the origin, so the animation should stay in place. 59 | To achieve that, I opened the Dope Sheet window, the Action Editor subwindow, and selected the `Y Location` track for the corresponding Armature_Monk node: 60 | 61 | ![](./tutorial/fixing_walk_animation_1.jpg) 62 | 63 | then: 64 | 65 | - pressed `Ctrl+Tab` to switch to the Graph Editor 66 | - pressed `Shift+H` to hide all the unselected nodes, leaving just the `Y Location` curve visible 67 | - `Right click on the selected nodes -> Snap -> "Selection to cursor value"` (as you can see the cursor Y position was set to 0, so this sets all keys in the animation to have a `Y Location` of 0): 68 | 69 | ![](./tutorial/fixing_walk_animation_2.jpg) 70 | 71 | - press `Alt+H` to unhide the hidden nodes 72 | 73 | The end results of this operation should be a walk animation that happens in place on the origin (please note I am not a Blender expert, so YMMV... but hopefully the end result is clear). 74 | Now save the resulting document as a new Blender file (I called it `animations.blend`), and then open the `CharacterTemplate.blend` file you copied earlier from the addon folder (please keep a separate copy from the one that comes with the add-on, so you can use the original to repeat this operation for other models if you need that). 75 | 76 | This file has a basic setup with an `Empty`, a `Camera` which is constrained to look at the `Empty`, and a `Light` that follows the camera around. This is your stage. 77 | Load the model that you originally uploaded to Mixamo using `File -> Import -> FBX`, then select the corresponding FBX file (you should have a copy also in the ZIP file you downloaded from Mixamo). 78 | 79 | The model should land at the origin, which is exactly where we want it: 80 | 81 | ![](./tutorial/preparing_for_export_1.jpg) 82 | 83 | Then, it's time to import the animations, by selecting `File -> Append`, select the previously saved `animations.blend` file, and navigate to the `Action` folder: 84 | 85 | ![](./tutorial/preparing_for_export_2.jpg) 86 | 87 | and finally select the desired animations and click on `Append`: 88 | 89 | ![](./tutorial/preparing_for_export_3.jpg) 90 | 91 | Finally, open the `Dope Sheet` window, the `Action Editor` subwindow, and for each of the imported animations: 92 | 93 | - select the animation: 94 | 95 | ![](./tutorial/preparing_for_export_4.jpg) 96 | 97 | - click on the `Push Down` button to create a NLA track for the animation: 98 | 99 | ![](./tutorial/preparing_for_export_5.jpg) 100 | 101 | The end result should look similar to this: 102 | 103 | ![](./tutorial/preparing_for_export_6.jpg) 104 | 105 | Your character should be set up so that it is facing towards the camera, and this will be the rendering of the animation heading South. 106 | 107 | Press `F12` to render your image and verify the animation fits the rendered viewport, the light is correct, the textures are correctly applied to the model etc. 108 | 109 | You can move the `Empty` object around to modify the `Camera` target, or change the lens `Orthographic Scale` setting of the camera to zoom in or out. 110 | 111 | As you will probably also need transparency to be enabled, verify the corresponding `Transparent` check box in the `Render properties -> Film` settings is enabled. 112 | Once everything looks fine, save your `.blend` file and get ready to export the spritesheets. 113 | 114 | ### Exporting the spritesheets 115 | 116 | Using Blender, open the `.blend` file you just created containing the model and desired animations, and switch to the `Scripting` window. 117 | Here, select `Text -> Open` and load the `render_8_direction_sprites.py` file you copied from the add-on folder. 118 | This script is in charge of rendering your animations at different angles and create a spritesheet description file that can be then imported in Godot. 119 | 120 | At the very beginning of the script you will find some variables that drive the export process. 121 | 122 | `OUTPUT_FOLDER` indicates the folder where all the frames will be stored (this will be created if not existing, otherwise make sure it is empty or you risk overwriting existing contents). 123 | 124 | `DEFAULT_LOOP` sets the looping flag for the exported animations (this is the default, you can change it later in the generated JBSS file if required). 125 | 126 | `DEFAULT_FPS` sets the resulting animations frames per second for the exported animations (this too can be changed later in the JBSS file). 127 | 128 | `OUTPUT_W` is the frame width, or the horizontal resolution of the rendered images. 129 | 130 | `OUTPUT_H` is the frame height, or the vertical resolution of the rendered images. 131 | 132 | `RENDERED_DIRECTIONS` is an array with the directions to render. The defaults are ok for 8-ways spritesheets, but you can set this to `[ "N", "E", "S", "W" ]` for 4-ways. 133 | 134 | `RENDERED_ANIMATIONS` is an array with the animations to render. These must match the names of the desired NLA tracks. 135 | 136 | `RENDER_EVERY_X_FRAMES` indicates how many frames to skip between rendered frames in each animation. The default is 1, so every frame will be rendered, but you can set it to 2 (render every other frame) or more to reduce the number of rendered images. 137 | 138 | `FORCE_RENDER_LAST_FRAME` if set to `True`, this will force the rendering of the last frame of the animation even if it would be skipped by the `RENDER_EVERY_X_FRAMES` configuration. This allows for smoother loops in some animations. 139 | 140 | `PROCESS_NORMALS` if set to `True` the normal map of each frame will be exported alongside the frame itself, and the corresponding animation will automatically be imported in Godot to be used as a normal map. 141 | 142 | Once the configuration of these variables is complete, you can select the object node to render (the parent of the NLA tracks) and press the `Run Script` button: 143 | 144 | ![Run the script](tutorial/run_the_script.jpg "Export the spritesheets!") 145 | 146 | While the process is running Blender will not respond to user inputs... don't worry, that's ok. Once the processing is complete, you will get a message in the Python console. 147 | Sometimes the script crashes Blender mentioning memory leaks if you switch from Blender to a different window during the rendering, at least on my Linux box... but fortunately this happens only after the export is complete, so it is not a showstopper. 148 | 149 | ### What does the script do? 150 | 151 | In a nutshell, the script will render all the frames of the selected animations, rotating the camera in 45 degrees steps to render all the selected directions, eventually also rendering the corresponding normal maps. 152 | It will create a hierarchy of folders within the specified `OUTPUT_FOLDER`, where the first level of the hierarchy is the name of the animation being rendered, and the second level is the direction. 153 | Within this second level folders you will find the frames (PNG files) and the normal maps (JPG files with a `normal_` prefix), numerically sorted. 154 | 155 | To render the normal maps, the script automatically sets up some nodes in the Blender Compositor. 156 | If you need some specific Compositor operations to suit you art style, you will have to change the script accordingly and create a dedicated output node. 157 | This is left as an exercise for the reader, as the code should be straightforward to follow (just look at the `PROCESS_NORMALS` handling). 158 | 159 | Finally, once the export process is complete, the script will create a JBSS file, which can be fed to Godot to import all the animations via this add-on. 160 | 161 | ### What is a JBSS file? 162 | 163 | Basically, something I made up to simplify importing animations into Godot. 164 | The add-on simply installs a new import handler specifically written to interpret JBSS files. 165 | These files are simple JSON documents containing a Dictionary, where the keys are the names of the animations rendered, and their subkeys are again Dictionaries for all the directions rendered. 166 | Within these sub-keys, there are currently two fields: 167 | - a number, `fps`, which indicates the frames per second for the specific animations 168 | - a boolean, `loop`, which indicates whether the animation must automatically loop or not 169 | 170 | ### How to use the animations in Godot 171 | 172 | Once you ran the Blender script successfully, you will have a folder containing a JBSS file and one or more subfolders containing the exported frames. 173 | Create a new Godot project, drag that folder in your new project, so that all individual frames will be imported, and then enable the add-on in your project settings: 174 | 175 | ![](tutorial/enable_add_on.jpg) 176 | 177 | this should trigger the import of the JBSS file. Then, create a new 2D scene and add an `AnimatedSprite2D` node to the scene: 178 | 179 | ![](Tutorial/example_scene.jpg) 180 | 181 | Finally, drag the `sprite_sheets.jbss` file from the file system to the `Sprite Frames` property of the `AnimatedSprite2D` node: 182 | 183 | ![](tutorial/importing_animations_in_godot.jpg) 184 | 185 | Now the `Animation` drop down should list all the imported animations, and the Animation Editor in the lower pane should show you all the imported frames for each animation. 186 | 187 | If you need to change the looping flag or the FPS for any animation, you can manually modify the corresponding `fps` and `loop` elements in the JBSS file and reimport it in Godot; your changes will be applied to the animations, without requiring you to manually do that from the Godot editor. For example, in this case I manually set the `loop` field for all the `attack_*` animations to `false` in `sprite_sheets.jbss`, as I did not want these to repeat. 188 | 189 | > Please beware of the fact that, as a consequence, if you manually change anything in these animations in Godot after importing them and then modify the JBSS file, Godot will reimport it overwriting your manual changes! 190 | 191 | Here's a hackish script you can use to test the imported animations: 192 | 193 | ```gdscript 194 | extends CharacterBody2D 195 | 196 | const SPEED = 200.0 197 | 198 | # Start facing south 199 | var _direction : int = 2 200 | 201 | var directions = [ 202 | "E", 203 | "SE", 204 | "S", 205 | "SW", 206 | "W", 207 | "NW", 208 | "N", 209 | "NE", 210 | ] 211 | 212 | # Silly state machine 213 | enum { MONK_WALKING, MONK_ATTACK, MONK_IDLE } 214 | var _status = MONK_WALKING 215 | 216 | 217 | func on_animation_finished(): 218 | _status = MONK_WALKING 219 | 220 | 221 | func on_frame_changed(): 222 | if _status == MONK_ATTACK: 223 | if $monk.get_frame() == 8: 224 | print("Emit fireball!") 225 | 226 | func _ready(): 227 | 228 | _status = MONK_WALKING 229 | $monk.animation_finished.connect(self.on_animation_finished) 230 | $monk.frame_changed.connect(self.on_frame_changed) 231 | $monk.set_animation("walk_S") 232 | 233 | func _physics_process(_delta): 234 | 235 | if not _status == MONK_ATTACK: 236 | # Get the input direction and handle the movement/deceleration. 237 | # As good practice, you should replace UI actions with custom gameplay actions. 238 | var direction_x = Input.get_axis("ui_left", "ui_right") 239 | if direction_x: 240 | velocity.x = direction_x * SPEED 241 | else: 242 | velocity.x = move_toward(velocity.x, 0, SPEED) 243 | 244 | var direction_y = Input.get_axis("ui_up", "ui_down") 245 | if direction_y: 246 | velocity.y = direction_y * SPEED 247 | else: 248 | velocity.y = move_toward(velocity.y, 0, SPEED) 249 | 250 | if direction_y == 0 and direction_x == 0: 251 | _status = MONK_IDLE 252 | $monk.animation = "idle_" + directions[_direction] 253 | $monk.play() 254 | 255 | else: 256 | var dir = Vector2(direction_x, direction_y) 257 | 258 | var angle = (2 * PI + dir.angle() + PI / 4) 259 | if angle > 2 * PI: 260 | angle -= 2 * PI 261 | _direction = clamp(round(angle / PI * 4) - 1, 0, 8) 262 | $monk.animation = "walk_" + directions[_direction] 263 | $monk.play() 264 | 265 | move_and_slide() 266 | 267 | # Handle attack. 268 | if Input.is_action_just_pressed("ui_accept"): 269 | _status = MONK_ATTACK 270 | velocity = Vector2.ZERO 271 | $monk.animation = "attack_" + directions[_direction] 272 | $monk.play() 273 | ``` 274 | 275 | The node hierarchy should be the following one, with the script attached to the `CharacterBody2D` node: 276 | 277 | ![](tutorial/node_hierarchy.jpg) 278 | 279 | and these are the results: 280 | 281 | ![](tutorial/example.gif) 282 | 283 | The light in Blender was a bit too harsh, or the model material a bit too shiny... but fixing those issues should be simple enough, now that the setup is ready. 284 | 285 | This is all, hopefully you managed to follow the tutorial and are now able to create your sprite assets a bit faster. Have fun! 286 | -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/blender/CharacterTemplate.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/blender/CharacterTemplate.blend -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/blender/render_8_direction_sprites.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | import math 4 | import json 5 | 6 | ###################################################################################### 7 | # Script parameters, change to suit your needs before running 8 | # PLEASE NOTE: remember to select the model in the Outliner before running the script! 9 | 10 | OUTPUT_FOLDER = '/home/massimiliano/projects/godot/tss/assets/images/monk' 11 | 12 | # Values used by Godot when importing: 13 | DEFAULT_LOOP = True # if True, all animations will be marked as Looping in Godot 14 | DEFAULT_FPS = 20 # this will be the animation speed applied to animations 15 | 16 | # Dimension of each rendered frame (height and width) 17 | OUTPUT_W = 128 18 | OUTPUT_H = 128 19 | 20 | # Choose the directions to render 21 | RENDERED_DIRECTIONS = [ "N", "NE", "E", "SE", "S", "SW", "W", "NW" ] 22 | 23 | # Choose the animations to render - these names should match NLA tracks 24 | RENDERED_ANIMATIONS = [ "idle", "walk" ] 25 | 26 | # Choose how many frames to skip per animation: 1 to render every frame 27 | RENDER_EVERY_X_FRAMES = 3 28 | 29 | # In case we're skipping frames and last frame would not be exported, 30 | # setting this to True will force rendering the last frame. This can 31 | # be useful to smooth looping animations 32 | FORCE_RENDER_LAST_FRAME = False 33 | 34 | # Create normal textures? 35 | PROCESS_NORMALS = True 36 | ###################################################################################### 37 | 38 | 39 | def render_image(i, counter, animation_folder, normal_file_output): 40 | 41 | s=bpy.context.scene 42 | s.frame_current = i 43 | 44 | render_file_path = ( 45 | animation_folder 46 | + "/" 47 | + str(counter).zfill(3) 48 | ) 49 | 50 | s.render.filepath = render_file_path 51 | 52 | if PROCESS_NORMALS: 53 | normal_file_output.base_path = animation_folder + "/" 54 | normal_file_output.file_slots[0].path = ("normal_" 55 | + str(counter).zfill(3) + "_###") 56 | 57 | bpy.ops.render.render( #{'dict': "override"}, 58 | #'INVOKE_DEFAULT', 59 | False, # undo support 60 | animation=False, 61 | write_still=True 62 | ) 63 | 64 | def render8directions_selected_objects(path): 65 | # path fixing 66 | path = os.path.abspath(path) 67 | 68 | # get list of selected objects 69 | selected_list = bpy.context.selected_objects 70 | 71 | # deselect all in scene 72 | bpy.ops.object.select_all(action='TOGGLE') 73 | 74 | s=bpy.context.scene 75 | 76 | s.render.resolution_x = OUTPUT_W 77 | s.render.resolution_y = OUTPUT_H 78 | 79 | # I left this in as in some of my models, I needed to translate the "root" object but 80 | # the animations were on the armature which I selected. 81 | # 82 | # obRoot = bpy.context.scene.objects["root"] 83 | 84 | s.use_nodes = True 85 | s.view_layers["ViewLayer"].use_pass_normal = True 86 | s.view_layers["ViewLayer"].use_pass_diffuse_color = True 87 | 88 | nodes = bpy.context.scene.node_tree.nodes 89 | links = bpy.context.scene.node_tree.links 90 | 91 | # Clear default nodes 92 | for n in nodes: 93 | nodes.remove(n) 94 | 95 | # Create input render layer node 96 | render_layers = nodes.new('CompositorNodeRLayers') 97 | normal_file_output = None 98 | 99 | if PROCESS_NORMALS: 100 | # generation of normals output 101 | 102 | # Create normal output nodes 103 | scale_node = nodes.new(type="CompositorNodeMixRGB") 104 | scale_node.blend_type = 'MULTIPLY' 105 | # scale_node.use_alpha = True 106 | scale_node.inputs[2].default_value = (0.5, 0.5, 0.5, 1) 107 | links.new(render_layers.outputs['Normal'], scale_node.inputs[1]) 108 | 109 | bias_node = nodes.new(type="CompositorNodeMixRGB") 110 | bias_node.blend_type = 'ADD' 111 | # bias_node.use_alpha = True 112 | bias_node.inputs[2].default_value = (0.5, 0.5, 0.5, 0) 113 | links.new(scale_node.outputs[0], bias_node.inputs[1]) 114 | 115 | normal_file_output = nodes.new(type="CompositorNodeOutputFile") 116 | normal_file_output.label = 'Normal Output' 117 | normal_file_output.file_slots[0].use_node_format = True 118 | normal_file_output.format.file_format = "JPEG" 119 | links.new(bias_node.outputs[0], normal_file_output.inputs[0]) 120 | 121 | # loop all initial selected objects (which will likely just be one obect.. I haven't tried setting up multiple yet) 122 | for o in selected_list: 123 | 124 | # select the object 125 | bpy.context.scene.objects[o.name].select_set(True) 126 | 127 | scn = bpy.context.scene 128 | camera = bpy.context.scene.objects["Camera"] 129 | # calculate the rotation radius 130 | camera_distance = math.sqrt(camera.location.x * camera.location.x + camera.location.y * camera.location.y) 131 | 132 | 133 | stored_actions = [] 134 | output_data = {} 135 | 136 | # loop through the actions 137 | for a in bpy.data.actions: 138 | #assign the action 139 | bpy.context.active_object.animation_data.action = bpy.data.actions.get(a.name) 140 | 141 | #dynamically set the last frame to render based on action 142 | scn.frame_end = int(bpy.context.active_object.animation_data.action.frame_range[1]) 143 | 144 | #set which actions you want to render. Make sure to use the exact name of the action! 145 | if (a.name in RENDERED_ANIMATIONS): 146 | 147 | output_data[a.name] = {} 148 | 149 | #create folder for animation 150 | action_folder = os.path.join(path, a.name) 151 | if not os.path.exists(action_folder): 152 | os.makedirs(action_folder) 153 | 154 | #loop through all 8 directions 155 | for angle in range(0, 360, 45): 156 | if angle == 0: 157 | angleDir = "W" 158 | if angle == 45: 159 | angleDir = "NW" 160 | if angle == 90: 161 | angleDir = "N" 162 | if angle == 135: 163 | angleDir = "NE" 164 | if angle == 180: 165 | angleDir = "E" 166 | if angle == 225: 167 | angleDir = "SE" 168 | if angle == 270: 169 | angleDir = "S" 170 | if angle == 315: 171 | angleDir = "SW" 172 | 173 | if (angleDir in RENDERED_DIRECTIONS): 174 | 175 | print("Processing ", a.name, angleDir) 176 | 177 | #create folder for specific angle 178 | animation_folder = os.path.join(action_folder, angleDir) 179 | if not os.path.exists(animation_folder): 180 | os.makedirs(animation_folder) 181 | 182 | #rotate the model for the new angle 183 | camera.location.x = camera_distance * math.cos(math.radians(angle)) 184 | camera.location.y = camera_distance * math.sin(math.radians(angle)) 185 | 186 | # the below is for the use case where the root needed to be translated. 187 | # obRoot.rotation_euler[2] = math.radians(angle) 188 | 189 | output_data[a.name][a.name + "_" + angleDir] = { "loop" : DEFAULT_LOOP, "fps" : DEFAULT_FPS } 190 | 191 | if PROCESS_NORMALS: 192 | output_data[a.name][a.name + "_" + angleDir + "_normal"] = { "loop" : DEFAULT_LOOP, "fps" : DEFAULT_FPS } 193 | 194 | 195 | #loop through and render frames. Can set how "often" it renders. 196 | #Every frame is likely not needed. 197 | counter = 0 198 | for i in range(s.frame_start,s.frame_end, RENDER_EVERY_X_FRAMES): 199 | 200 | render_image(i, counter, animation_folder, normal_file_output) 201 | counter += 1 202 | 203 | if i != s.frame_end and FORCE_RENDER_LAST_FRAME: 204 | render_image(s.frame_end, counter, animation_folder, normal_file_output) 205 | 206 | # Reset camera in the end 207 | camera.location.x = camera_distance * math.cos(math.radians(270)) 208 | camera.location.y = camera_distance * math.sin(math.radians(270)) 209 | 210 | if len(output_data.keys()) > 0: 211 | with open(os.path.join(path,'sprite_sheets.jbss'),'w') as godot_resource: 212 | json.dump(output_data, godot_resource, sort_keys=True, indent=4) 213 | 214 | 215 | render8directions_selected_objects(OUTPUT_FOLDER) 216 | 217 | -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/blenderspritesheetimporter.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | var import_plugin 5 | 6 | func _enter_tree(): 7 | import_plugin = preload("jbss_importer.gd").new() 8 | add_import_plugin(import_plugin) 9 | 10 | 11 | func _exit_tree(): 12 | remove_import_plugin(import_plugin) 13 | import_plugin = null 14 | -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/icon.png -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/jbss_importer.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorImportPlugin 3 | 4 | enum Presets { DEFAULT } 5 | 6 | # Turn this to true in case you want details about the import process 7 | var verbose : bool = false 8 | 9 | func _get_importer_name(): 10 | return "OddMode.blenderspritesheet" 11 | 12 | func _get_visible_name(): 13 | return "Blender Sprite Sheet" 14 | 15 | func _get_recognized_extensions(): 16 | return ["jbss"] 17 | 18 | func _get_save_extension(): 19 | return "tres" 20 | 21 | func _get_resource_type(): 22 | return "SpriteFrames" 23 | 24 | func _get_preset_count(): 25 | return Presets.size() 26 | 27 | func _get_priority(): 28 | return 1.0 29 | 30 | func _get_import_order(): 31 | return 1.0 32 | 33 | func _get_preset_name(preset_index): 34 | match preset_index: 35 | Presets.DEFAULT: 36 | return "Default" 37 | _: 38 | return "Unknown" 39 | 40 | func _get_import_options(path, preset_index): 41 | return [] 42 | 43 | func _get_option_visibility(path, option_name, options): 44 | return false 45 | 46 | func debug_print(str): 47 | if verbose: 48 | print(str) 49 | 50 | func import_animation_frames(animation_path : String, animation_name : String, animation : SpriteFrames, subfolder_data : Variant): 51 | 52 | var frames : Array = [] 53 | var frames_normal : Array = [] 54 | var dir = DirAccess.open(animation_path) 55 | 56 | if dir: 57 | dir.list_dir_begin() 58 | var file_name = dir.get_next() 59 | while file_name != "": 60 | if not dir.current_is_dir(): 61 | if not file_name.contains(".import"): 62 | if file_name.contains("normal"): 63 | frames_normal.append(file_name) 64 | else: 65 | frames.append(file_name) 66 | 67 | file_name = dir.get_next() 68 | 69 | if frames.size() > 0: 70 | frames.sort() 71 | animation.add_animation(animation_name) 72 | animation.set_animation_speed(animation_name, subfolder_data[animation_name]["fps"]) 73 | animation.set_animation_loop(animation_name, subfolder_data[animation_name]["loop"]) 74 | 75 | for frame_file in frames: 76 | debug_print("Adding frame: " + animation_path + "/" + frame_file) 77 | animation.add_frame(animation_name, load(animation_path + "/" + frame_file)) 78 | 79 | if frames_normal.size() > 0: 80 | frames_normal.sort() 81 | animation_name += "_normal" 82 | animation.add_animation(animation_name) 83 | animation.set_animation_speed(animation_name, subfolder_data[animation_name]["fps"]) 84 | animation.set_animation_loop(animation_name, subfolder_data[animation_name]["loop"]) 85 | 86 | for frame_file in frames_normal: 87 | debug_print("Adding frame: " + animation_path + "/" + frame_file) 88 | animation.add_frame(animation_name, load(animation_path + "/" + frame_file)) 89 | 90 | func import_folder(path : String, animation_name : String, animation : SpriteFrames, subfolder_data : Variant): 91 | var animation_folder = path + "/" + animation_name 92 | var dir = DirAccess.open(animation_folder) 93 | if dir: 94 | dir.list_dir_begin() 95 | var file_name = dir.get_next() 96 | while file_name != "": 97 | if dir.current_is_dir(): 98 | debug_print("Processing animation: " + animation_name + "_" + file_name) 99 | import_animation_frames(animation_folder + "/" + file_name, animation_name + "_" + file_name, animation, subfolder_data) 100 | else: 101 | debug_print("Ignoring file: " + file_name) 102 | 103 | file_name = dir.get_next() 104 | 105 | else: 106 | push_error("An error occurred when trying to access " + animation_folder) 107 | 108 | 109 | func _import(source_file, save_path, options, r_platform_variants, r_gen_files): 110 | 111 | var file = FileAccess.open(source_file, FileAccess.READ) 112 | if file == null: 113 | return FileAccess.get_open_error() 114 | file.close() 115 | 116 | var json = JSON.new() 117 | var error = json.parse(file.get_file_as_string(source_file)) 118 | if error != OK: 119 | push_error("JSON Parse Error: ", json.get_error_message(), " in ", source_file, " at line ", json.get_error_line()) 120 | return ERR_PARSE_ERROR 121 | 122 | var data_received = json.data 123 | 124 | 125 | var output : SpriteFrames = SpriteFrames.new() 126 | output.remove_animation("default") 127 | 128 | var subfolders = data_received.keys() 129 | if subfolders.size() == 0: 130 | # Empty file? 131 | return ERR_PARSE_ERROR 132 | 133 | var import_root = file.get_path().get_base_dir() 134 | for i in subfolders: 135 | import_folder(import_root, i, output, data_received[i]) 136 | 137 | return ResourceSaver.save(output, "%s.%s" % [save_path, _get_save_extension()]) 138 | -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Blender SpriteSheet Importer" 4 | description="Import n-directional sprite sheets generated by Blender" 5 | author="OddMode" 6 | version="0.1" 7 | script="blenderspritesheetimporter.gd" 8 | -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/.gdignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/.gdignore -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/animation_import.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/animation_import.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/enable_add_on.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/enable_add_on.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/example.gif -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/example_scene.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/example_scene.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/export_to_mixamo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/export_to_mixamo.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/fixing_walk_animation_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/fixing_walk_animation_1.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/fixing_walk_animation_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/fixing_walk_animation_2.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/import_plugin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/import_plugin.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/importing_animations_in_godot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/importing_animations_in_godot.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/install_plugin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/install_plugin.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/mixamo_export_settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/mixamo_export_settings.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/node_hierarchy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/node_hierarchy.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/preparing_for_export_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/preparing_for_export_1.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/preparing_for_export_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/preparing_for_export_2.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/preparing_for_export_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/preparing_for_export_3.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/preparing_for_export_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/preparing_for_export_4.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/preparing_for_export_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/preparing_for_export_5.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/preparing_for_export_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/preparing_for_export_6.jpg -------------------------------------------------------------------------------- /addons/blenderspritesheetimporter/tutorial/run_the_script.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddmode-studio/BlenderSpriteSheetImporter/2c93b226e6bb4a3e8da7c003ea3e80c288647b53/addons/blenderspritesheetimporter/tutorial/run_the_script.jpg --------------------------------------------------------------------------------