├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── assets ├── shooting_squares.blend ├── shooting_squares.glb └── shooting_squares.gltf ├── bevy_blender_utils ├── __init__.py └── shaders │ ├── simple_color.frag │ ├── simple_color.vert │ ├── vector3.frag │ └── vector3.vert ├── examples └── shooting_squares.rs ├── images └── showcase.png └── src ├── bbu_manager.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | .idea -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_blender_utils" 3 | version = "0.2.0" 4 | edition = "2021" 5 | description = "Bevy companion plugin for bevy_blender_utils Blender addon" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/kulkalkul/bevy_blender_utils.git" 8 | homepage = "https://github.com/kulkalkul/bevy_blender_utils.git" 9 | documentation = "https://docs.rs/bevy_blender_utils" 10 | readme = "README.md" 11 | keywords = [ "bevy", "gamedev", "asset", "blender" ] 12 | categories = [ "game-development" ] 13 | authors = [ "Bora Ülker" ] 14 | exclude = [ "images", "assets", "bevy_blender_utils" ] 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | 18 | [dependencies] 19 | bevy = "0.11.0" 20 | serde = "1.0.154" 21 | serde_json = "1.0.94" 22 | thiserror = "1.0.39" 23 | 24 | [dev-dependencies] 25 | bevy = "0.11.0" 26 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Bora Ülker 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Bora Ülker 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy_blender_utils 2 | 3 | ![images/showcase.png](images/showcase.png) 4 | 5 | This is influenced by both [bevy-scene-hook](https://github.com/nicopap/bevy-scene-hook) and 6 | [blender_bevy_toolkit](https://github.com/sdfgeoff/blender_bevy_toolkit). 7 | 8 | **This is a proof-of-concept at this point. I need to dogfood this, but my project is still on 0.9.** 9 | 10 | ## What this is and what this isn't? 11 | 12 | BBU (Blender Bevy Utils) consist of two parts; 13 | - BBU Addon: A Blender addon that allows extra data to be set on objects. 14 | - BBU Companion Crate: An opinionated way of using data generated by the addon. 15 | 16 | This repo includes both source codes. 17 | 18 | The Aim of BBU is to allow prefab-like workflow while still maintaining the code-first approach of editor-less Bevy. 19 | BBU only provides simple primitives like f32, Vec3 or Cuboid; while instantiating components with it is done through 20 | code. While this still requires some work to be done manually using code, it provides visual feedback using Blender as 21 | an editor and a clear asset workflow to follow. I prefer this because (my opinionated thoughts); 22 | 23 | - Parsing components from GLTF is too much work. 24 | - Blender as a base isn't enough for a game engine-like experience; making it using limited addon functionality is too 25 | much work. 26 | 27 | So, this is more like ``bevy-scene-hook``'s approach; and can be used with it without this companion crate. I don't 28 | know if it is possible to do the hot-reload with my approach. 29 | 30 | ## Installation of addon 31 | 32 | You can clone the repository, zip [bevy_blender_utils](bevy_blender_utils) and import it as a Blender addon. 33 | 34 | ## Usage 35 | 36 | Usage is user-dependent, but the workflow I prefer can be found on [examples](examples). In short, it is like this: 37 | - Add plugin. 38 | - Have an id field for all scene-objects that needs care. 39 | - Create an enum and add a ``snake_case`` deserializer with ``id`` being the tag. 40 | - Profit. 41 | 42 | Addon can be used with library override, which is **awesome**. Please open an issue if you need documentation on it! 43 | 44 | ## Bevy Compatibility 45 | 46 | | Bevy Version | Crate Version | Plugin Version | 47 | |--------------|---------------|----------------| 48 | | `0.11` | `0.2` | `0.1` | 49 | | `0.10` | `0.1` | `0.1` | 50 | 51 | ## Blender Compatibility 52 | 53 | Minimum Blender version: `3.4.0` 54 | 55 | 56 | ## Todo 57 | - Tidy up python code and document it (there are lots of edge cases handled). 58 | - Maybe improve documentation, if possible. 59 | - Hide ``Id`` for basic usage. 60 | - Multi-color support. 61 | - Explore hot-reload possibility. 62 | 63 | ## License 64 | 65 | Licensed under either of 66 | 67 | * Apache License, Version 2.0 68 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 69 | * MIT license 70 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 71 | 72 | at your option. 73 | 74 | ## Contribution 75 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, 76 | as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. -------------------------------------------------------------------------------- /assets/shooting_squares.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulkalkul/bevy_blender_utils/8160f5e60b3df956bbc2e17278a326aee899e050/assets/shooting_squares.blend -------------------------------------------------------------------------------- /assets/shooting_squares.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulkalkul/bevy_blender_utils/8160f5e60b3df956bbc2e17278a326aee899e050/assets/shooting_squares.glb -------------------------------------------------------------------------------- /assets/shooting_squares.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset" : { 3 | "generator" : "Khronos glTF Blender I/O v3.4.49", 4 | "version" : "2.0" 5 | }, 6 | "scene" : 0, 7 | "scenes" : [ 8 | { 9 | "extras" : { 10 | "bc" : { 11 | "snap" : { 12 | "hit" : 0 13 | } 14 | } 15 | }, 16 | "name" : "Scene", 17 | "nodes" : [ 18 | 2, 19 | 3 20 | ] 21 | } 22 | ], 23 | "nodes" : [ 24 | { 25 | "extras" : { 26 | "hops" : {}, 27 | "bbu_object_data" : { 28 | "id" : "shooting_square", 29 | "spawn_point" : [ 30 | 0.0, 31 | 0.5, 32 | 0.0 33 | ], 34 | "speed" : 1.5 35 | } 36 | }, 37 | "mesh" : 0, 38 | "name" : "FasterCube.001", 39 | "translation" : [ 40 | -1.724906086921692, 41 | -0.43343526124954224, 42 | 1.3050156831741333 43 | ] 44 | }, 45 | { 46 | "children" : [ 47 | 0 48 | ], 49 | "extras" : { 50 | "hops" : {}, 51 | "bbu_object_data" : { 52 | "id" : "shooting_square", 53 | "spawn_point" : [ 54 | 0.0, 55 | 0.5, 56 | 0.0 57 | ], 58 | "speed" : 0.75 59 | } 60 | }, 61 | "mesh" : 1, 62 | "name" : "SlowerCube.001", 63 | "translation" : [ 64 | 1.4028853178024292, 65 | 0, 66 | 0 67 | ] 68 | }, 69 | { 70 | "children" : [ 71 | 1 72 | ], 73 | "extras" : { 74 | "hops" : {}, 75 | "bbu_object_data" : { 76 | "id" : "shooting_square", 77 | "spawn_point" : [ 78 | 0.0, 79 | 0.5, 80 | 0.0 81 | ], 82 | "speed" : 0.75 83 | } 84 | }, 85 | "mesh" : 2, 86 | "name" : "SlowerCube", 87 | "rotation" : [ 88 | 0, 89 | 0.7071068286895752, 90 | 0, 91 | 0.7071067094802856 92 | ], 93 | "translation" : [ 94 | 0.75, 95 | 0, 96 | -0.3631393313407898 97 | ] 98 | }, 99 | { 100 | "extras" : { 101 | "hops" : {}, 102 | "bbu_object_data" : { 103 | "id" : "shooting_square", 104 | "spawn_point" : [ 105 | 0.0, 106 | 0.5, 107 | 0.0 108 | ], 109 | "speed" : 1.5 110 | } 111 | }, 112 | "mesh" : 3, 113 | "name" : "FasterCube", 114 | "rotation" : [ 115 | 0, 116 | 0.7071068286895752, 117 | 0, 118 | 0.7071067094802856 119 | ], 120 | "translation" : [ 121 | -0.75, 122 | 0, 123 | 0 124 | ] 125 | } 126 | ], 127 | "materials" : [ 128 | { 129 | "doubleSided" : true, 130 | "name" : "Material.001", 131 | "pbrMetallicRoughness" : { 132 | "baseColorFactor" : [ 133 | 0.800000011920929, 134 | 0.800000011920929, 135 | 0.800000011920929, 136 | 1 137 | ], 138 | "metallicFactor" : 0, 139 | "roughnessFactor" : 0.5 140 | } 141 | } 142 | ], 143 | "meshes" : [ 144 | { 145 | "extras" : { 146 | "hops" : {} 147 | }, 148 | "name" : "Cube.004", 149 | "primitives" : [ 150 | { 151 | "attributes" : { 152 | "POSITION" : 0, 153 | "TEXCOORD_0" : 1, 154 | "NORMAL" : 2 155 | }, 156 | "indices" : 3, 157 | "material" : 0 158 | } 159 | ] 160 | }, 161 | { 162 | "extras" : { 163 | "hops" : {} 164 | }, 165 | "name" : "Cube.003", 166 | "primitives" : [ 167 | { 168 | "attributes" : { 169 | "POSITION" : 4, 170 | "TEXCOORD_0" : 5, 171 | "NORMAL" : 6 172 | }, 173 | "indices" : 3, 174 | "material" : 0 175 | } 176 | ] 177 | }, 178 | { 179 | "extras" : { 180 | "hops" : {} 181 | }, 182 | "name" : "Cube.001", 183 | "primitives" : [ 184 | { 185 | "attributes" : { 186 | "POSITION" : 7, 187 | "TEXCOORD_0" : 8, 188 | "NORMAL" : 9 189 | }, 190 | "indices" : 3, 191 | "material" : 0 192 | } 193 | ] 194 | }, 195 | { 196 | "extras" : { 197 | "hops" : {} 198 | }, 199 | "name" : "Cube.002", 200 | "primitives" : [ 201 | { 202 | "attributes" : { 203 | "POSITION" : 10, 204 | "TEXCOORD_0" : 11, 205 | "NORMAL" : 12 206 | }, 207 | "indices" : 3, 208 | "material" : 0 209 | } 210 | ] 211 | } 212 | ], 213 | "accessors" : [ 214 | { 215 | "bufferView" : 0, 216 | "componentType" : 5126, 217 | "count" : 24, 218 | "max" : [ 219 | 0.5, 220 | 0.5, 221 | 0.5 222 | ], 223 | "min" : [ 224 | -0.5, 225 | -0.5, 226 | -0.5 227 | ], 228 | "type" : "VEC3" 229 | }, 230 | { 231 | "bufferView" : 1, 232 | "componentType" : 5126, 233 | "count" : 24, 234 | "type" : "VEC2" 235 | }, 236 | { 237 | "bufferView" : 2, 238 | "componentType" : 5126, 239 | "count" : 24, 240 | "type" : "VEC3" 241 | }, 242 | { 243 | "bufferView" : 3, 244 | "componentType" : 5123, 245 | "count" : 36, 246 | "type" : "SCALAR" 247 | }, 248 | { 249 | "bufferView" : 4, 250 | "componentType" : 5126, 251 | "count" : 24, 252 | "max" : [ 253 | 0.5, 254 | 0.5, 255 | 0.5 256 | ], 257 | "min" : [ 258 | -0.5, 259 | -0.5, 260 | -0.5 261 | ], 262 | "type" : "VEC3" 263 | }, 264 | { 265 | "bufferView" : 5, 266 | "componentType" : 5126, 267 | "count" : 24, 268 | "type" : "VEC2" 269 | }, 270 | { 271 | "bufferView" : 6, 272 | "componentType" : 5126, 273 | "count" : 24, 274 | "type" : "VEC3" 275 | }, 276 | { 277 | "bufferView" : 7, 278 | "componentType" : 5126, 279 | "count" : 24, 280 | "max" : [ 281 | 0.5, 282 | 0.5, 283 | 0.5 284 | ], 285 | "min" : [ 286 | -0.5, 287 | -0.5, 288 | -0.5 289 | ], 290 | "type" : "VEC3" 291 | }, 292 | { 293 | "bufferView" : 8, 294 | "componentType" : 5126, 295 | "count" : 24, 296 | "type" : "VEC2" 297 | }, 298 | { 299 | "bufferView" : 9, 300 | "componentType" : 5126, 301 | "count" : 24, 302 | "type" : "VEC3" 303 | }, 304 | { 305 | "bufferView" : 10, 306 | "componentType" : 5126, 307 | "count" : 24, 308 | "max" : [ 309 | 0.5, 310 | 0.5, 311 | 0.5 312 | ], 313 | "min" : [ 314 | -0.5, 315 | -0.5, 316 | -0.5 317 | ], 318 | "type" : "VEC3" 319 | }, 320 | { 321 | "bufferView" : 11, 322 | "componentType" : 5126, 323 | "count" : 24, 324 | "type" : "VEC2" 325 | }, 326 | { 327 | "bufferView" : 12, 328 | "componentType" : 5126, 329 | "count" : 24, 330 | "type" : "VEC3" 331 | } 332 | ], 333 | "bufferViews" : [ 334 | { 335 | "buffer" : 0, 336 | "byteLength" : 288, 337 | "byteOffset" : 0, 338 | "target" : 34962 339 | }, 340 | { 341 | "buffer" : 0, 342 | "byteLength" : 192, 343 | "byteOffset" : 288, 344 | "target" : 34962 345 | }, 346 | { 347 | "buffer" : 0, 348 | "byteLength" : 288, 349 | "byteOffset" : 480, 350 | "target" : 34962 351 | }, 352 | { 353 | "buffer" : 0, 354 | "byteLength" : 72, 355 | "byteOffset" : 768, 356 | "target" : 34963 357 | }, 358 | { 359 | "buffer" : 0, 360 | "byteLength" : 288, 361 | "byteOffset" : 840, 362 | "target" : 34962 363 | }, 364 | { 365 | "buffer" : 0, 366 | "byteLength" : 192, 367 | "byteOffset" : 1128, 368 | "target" : 34962 369 | }, 370 | { 371 | "buffer" : 0, 372 | "byteLength" : 288, 373 | "byteOffset" : 1320, 374 | "target" : 34962 375 | }, 376 | { 377 | "buffer" : 0, 378 | "byteLength" : 288, 379 | "byteOffset" : 1608, 380 | "target" : 34962 381 | }, 382 | { 383 | "buffer" : 0, 384 | "byteLength" : 192, 385 | "byteOffset" : 1896, 386 | "target" : 34962 387 | }, 388 | { 389 | "buffer" : 0, 390 | "byteLength" : 288, 391 | "byteOffset" : 2088, 392 | "target" : 34962 393 | }, 394 | { 395 | "buffer" : 0, 396 | "byteLength" : 288, 397 | "byteOffset" : 2376, 398 | "target" : 34962 399 | }, 400 | { 401 | "buffer" : 0, 402 | "byteLength" : 192, 403 | "byteOffset" : 2664, 404 | "target" : 34962 405 | }, 406 | { 407 | "buffer" : 0, 408 | "byteLength" : 288, 409 | "byteOffset" : 2856, 410 | "target" : 34962 411 | } 412 | ], 413 | "buffers" : [ 414 | { 415 | "byteLength" : 3144, 416 | "uri" : "data:application/octet-stream;base64,AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AAAAPgAAAD8AAMA+AABAPwAAwD4AAEA/AAAgPwAAAAAAACA/AACAPwAAYD8AAIA+AAAAPgAAgD4AAMA+AAAAAAAAwD4AAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAQAOABQAAQAUAAcACgAGABIACgASABYAFwATAAwAFwAMABAADwADAAkADwAJABUABQACAAgABQAIAAsAEQANAAAAEQAAAAQAAAAAPwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AAAAPgAAAD8AAMA+AABAPwAAwD4AAEA/AAAgPwAAAAAAACA/AACAPwAAYD8AAIA+AAAAPgAAgD4AAMA+AAAAAAAAwD4AAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAPwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AAAAPgAAAD8AAMA+AABAPwAAwD4AAEA/AAAgPwAAAAAAACA/AACAPwAAYD8AAIA+AAAAPgAAgD4AAMA+AAAAAAAAwD4AAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAPwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AAAAPgAAAD8AAMA+AABAPwAAwD4AAEA/AAAgPwAAAAAAACA/AACAPwAAYD8AAIA+AAAAPgAAgD4AAMA+AAAAAAAAwD4AAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACA" 417 | } 418 | ] 419 | } 420 | -------------------------------------------------------------------------------- /bevy_blender_utils/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import math 3 | 4 | import bpy 5 | import gpu 6 | from gpu.types import GPUShader 7 | from gpu_extras.batch import batch_for_shader 8 | 9 | bl_info = { 10 | "name": "Bevy Blender Utils", 11 | "blender": (3, 4, 0), 12 | "category": "Game Engine", 13 | } 14 | 15 | 16 | class BBU_PROPERTIES_UL_List(bpy.types.UIList): 17 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname): 18 | if self.layout_type in {"DEFAULT", "COMPACT"}: 19 | column = layout.column() 20 | row = column.row() 21 | row.label(text=item.id) 22 | 23 | if item.type == "string": 24 | text = "\"{}\"".format(item.string) 25 | row.label(text=text) 26 | elif item.type == "bool": 27 | text = "{}".format(item.bool) 28 | row.label(text=text) 29 | elif item.type == "integer": 30 | text = "{}".format(item.integer) 31 | row.label(text=text) 32 | elif item.type == "float": 33 | text = "{:.2f}".format(item.float) 34 | row.label(text=text) 35 | elif item.type == "vector3": 36 | text = "x: {:.2f} y: {:.2f} z: {:.2f}".format( 37 | item.vector3_x, 38 | item.vector3_y, 39 | item.vector3_z, 40 | ) 41 | row.label(text=text) 42 | elif item.type == "cuboid": 43 | text = "x: {:.2f} y: {:.2f} z: {:.2f}".format( 44 | item.cuboid_x, 45 | item.cuboid_y, 46 | item.cuboid_z, 47 | ) 48 | row.label(text=text) 49 | elif item.type == "sphere": 50 | text = "radius: {:.2f}".format(item.radius) 51 | row.label(text=text) 52 | elif item.type == "capsule": 53 | text = "radius: {:.2f} height: {:.2f}".format(item.radius, item.height) 54 | row.label(text=text) 55 | elif self.layout_type == "GRID": 56 | layout.label(text="") 57 | 58 | 59 | class BBU_PROPERTIES_OT_AddProperty(bpy.types.Operator): 60 | bl_idname = "bbu_properties.add_property" 61 | bl_label = "Add new property" 62 | 63 | def execute(self, context): 64 | properties = context.object.bbu_properties 65 | properties.add() 66 | context.object.bbu_properties_index = len(properties) - 1 67 | return {"FINISHED"} 68 | 69 | 70 | class BBU_PROPERTIES_OT_RemoveProperty(bpy.types.Operator): 71 | bl_idname = "bbu_properties.remove_property" 72 | bl_label = "Remove property" 73 | 74 | @classmethod 75 | def poll(cls, context): 76 | return context.object.bbu_properties 77 | 78 | def execute(self, context): 79 | properties = context.object.bbu_properties 80 | index = context.object.bbu_properties_index 81 | 82 | properties.remove(index) 83 | context.object.bbu_properties_index = min(max(0, index - 1), len(properties) - 1) 84 | 85 | return {"FINISHED"} 86 | 87 | 88 | class BBU_PROPERTIES_OT_MoveProperty(bpy.types.Operator): 89 | bl_idname = "bbu_properties.move_property" 90 | bl_label = "Move property" 91 | 92 | direction: bpy.props.EnumProperty( 93 | items=[ 94 | ("UP", "Up", ""), 95 | ("DOWN", "Down", ""), 96 | ], 97 | ) 98 | 99 | @classmethod 100 | def poll(cls, context): 101 | return context.object.bbu_properties 102 | 103 | def execute(self, context): 104 | properties = context.object.bbu_properties 105 | index = context.object.bbu_properties_index 106 | 107 | neighbour = index + (-1 if self.direction == "UP" else 1) 108 | properties.move(neighbour, index) 109 | 110 | length = len(properties) - 1 111 | context.object.bbu_properties_index = max(0, min(neighbour, length)) 112 | 113 | return {"FINISHED"} 114 | 115 | 116 | item_types = [ 117 | ("string", "String", "String"), 118 | ("bool", "Boolean", "Boolean"), 119 | ("integer", "Integer", "Integer"), 120 | ("float", "Float", "Float"), 121 | ("vector3", "Vector3", "Vector3"), 122 | ("cuboid", "Cuboid", "Cuboid"), 123 | ("sphere", "Sphere", "Sphere"), 124 | ("capsule", "Capsule", "Capsule"), 125 | ] 126 | 127 | up_vectors = [ 128 | ("zp", "Z+", "Z+"), 129 | ("yp", "Y+", "Y+"), 130 | ("xp", "X+", "Z+"), 131 | ] 132 | 133 | defaults = { 134 | "id": "unnamed", 135 | "type": "string", 136 | "string": "", 137 | "bool": False, 138 | "integer": 0, 139 | "float": 0.0, 140 | "vector3_x": 0.0, 141 | "vector3_y": 0.0, 142 | "vector3_z": 0.0, 143 | "offset_x": 0.0, 144 | "offset_y": 0.0, 145 | "offset_z": 0.0, 146 | "cuboid_x": 0.5, 147 | "cuboid_y": 0.5, 148 | "cuboid_z": 0.5, 149 | "radius": 0.5, 150 | "height": 1.0, 151 | "up_vector": "zp", 152 | } 153 | 154 | 155 | def del_if_exists(obj, prop): 156 | if prop in obj: 157 | del obj[prop] 158 | 159 | 160 | def update_value(self, _context): 161 | del_if_exists(self, "string") 162 | del_if_exists(self, "bool") 163 | del_if_exists(self, "integer") 164 | del_if_exists(self, "float") 165 | 166 | del_if_exists(self, "vector3_x") 167 | del_if_exists(self, "vector3_y") 168 | del_if_exists(self, "vector3_z") 169 | 170 | del_if_exists(self, "offset_x") 171 | del_if_exists(self, "offset_y") 172 | del_if_exists(self, "offset_z") 173 | 174 | del_if_exists(self, "cuboid_x") 175 | del_if_exists(self, "cuboid_y") 176 | del_if_exists(self, "cuboid_z") 177 | 178 | del_if_exists(self, "radius") 179 | del_if_exists(self, "height") 180 | 181 | 182 | class BBUDataListItem(bpy.types.PropertyGroup): 183 | id: bpy.props.StringProperty(name="id", default="unnamed") 184 | type: bpy.props.EnumProperty(items=item_types, default="string", update=update_value) 185 | string: bpy.props.StringProperty(name="string", default=defaults["string"]) 186 | bool: bpy.props.BoolProperty(name="bool", default=defaults["bool"]) 187 | integer: bpy.props.IntProperty(name="integer", default=defaults["integer"]) 188 | float: bpy.props.FloatProperty(name="float", default=defaults["float"]) 189 | 190 | vector3_x: bpy.props.FloatProperty(name="vector3_x", default=defaults["vector3_x"]) 191 | vector3_y: bpy.props.FloatProperty(name="vector3_y", default=defaults["vector3_y"]) 192 | vector3_z: bpy.props.FloatProperty(name="vector3_z", default=defaults["vector3_z"]) 193 | 194 | offset_x: bpy.props.FloatProperty(name="offset_x", default=defaults["offset_x"]) 195 | offset_y: bpy.props.FloatProperty(name="offset_y", default=defaults["offset_y"]) 196 | offset_z: bpy.props.FloatProperty(name="offset_z", default=defaults["offset_z"]) 197 | 198 | cuboid_x: bpy.props.FloatProperty(name="cuboid_x", default=defaults["cuboid_x"]) 199 | cuboid_y: bpy.props.FloatProperty(name="cuboid_y", default=defaults["cuboid_y"]) 200 | cuboid_z: bpy.props.FloatProperty(name="cuboid_z", default=defaults["cuboid_z"]) 201 | 202 | radius: bpy.props.FloatProperty(name="radius", default=defaults["radius"]) 203 | height: bpy.props.FloatProperty(name="height", default=defaults["height"]) 204 | up_vector: bpy.props.EnumProperty(items=up_vectors, default=defaults["up_vector"]) 205 | 206 | 207 | class BBUPanel(bpy.types.Panel): 208 | bl_idname = "OBJECT_PT_bbu_panel" 209 | bl_label = "Bevy Blender Utils" 210 | bl_space_type = "PROPERTIES" 211 | bl_region_type = "WINDOW" 212 | bl_context = "object" 213 | 214 | def draw(self, context): 215 | layout = self.layout 216 | obj = context.object 217 | 218 | properties = obj.bbu_properties 219 | index = obj.bbu_properties_index 220 | 221 | row = layout.row() 222 | 223 | row.prop(obj, "bbu_visualization", text="Draw Visualization", toggle=True) 224 | column = row.column() 225 | column.prop(obj, "bbu_visualization_show_all", text="Show All", toggle=True) 226 | column.enabled = obj.bbu_visualization 227 | 228 | row = layout.row() 229 | row.template_list( 230 | "BBU_PROPERTIES_UL_List", "Bevy Properties", obj, "bbu_properties", obj, "bbu_properties_index" 231 | ) 232 | 233 | row = layout.row() 234 | row.operator("bbu_properties.add_property", text="Add Property") 235 | row.operator("bbu_properties.remove_property", text="Remove Property") 236 | 237 | row = layout.row() 238 | row.operator("bbu_properties.move_property", text="Move Up").direction = "UP" 239 | row.operator("bbu_properties.move_property", text="Move Down").direction = "DOWN" 240 | 241 | if index < 0: 242 | return 243 | if index >= len(properties): 244 | return 245 | 246 | item = properties[index] 247 | 248 | layout.separator() 249 | 250 | ids = list(map(lambda x: x.id, properties)) 251 | 252 | if len(ids) != len(set(ids)): 253 | row = layout.row() 254 | row.label(text="IDs must be unique!") 255 | 256 | row = layout.row() 257 | row.prop(item, "type", text="Type") 258 | row.prop(item, "id", text="ID") 259 | 260 | row = layout.row() 261 | 262 | if item.type == "string": 263 | row.prop(item, "string", text="Value") 264 | elif item.type == "bool": 265 | row.prop(item, "bool", text="Value") 266 | elif item.type == "integer": 267 | row.prop(item, "integer", text="Value") 268 | elif item.type == "float": 269 | row.prop(item, "float", text="Value") 270 | elif item.type == "vector3": 271 | row.prop(item, "vector3_x", text="x") 272 | row.prop(item, "vector3_y", text="y") 273 | row.prop(item, "vector3_z", text="z") 274 | elif item.type == "cuboid": 275 | row.prop(item, "cuboid_x", text="x") 276 | row.prop(item, "cuboid_y", text="y") 277 | row.prop(item, "cuboid_z", text="z") 278 | row = layout.row() 279 | row.prop(item, "offset_x", text="x") 280 | row.prop(item, "offset_y", text="y") 281 | row.prop(item, "offset_z", text="z") 282 | elif item.type == "sphere": 283 | row.prop(item, "radius", text="Radius") 284 | row = layout.row() 285 | row.prop(item, "offset_x", text="x") 286 | row.prop(item, "offset_y", text="y") 287 | row.prop(item, "offset_z", text="z") 288 | elif item.type == "capsule": 289 | row.prop(item, "up_vector", text="Up Vector") 290 | row = layout.row() 291 | row.prop(item, "radius", text="Radius") 292 | row.prop(item, "height", text="Height") 293 | row = layout.row() 294 | row.prop(item, "offset_x", text="x") 295 | row.prop(item, "offset_y", text="y") 296 | row.prop(item, "offset_z", text="z") 297 | 298 | 299 | classes = ( 300 | BBU_PROPERTIES_UL_List, 301 | BBU_PROPERTIES_OT_AddProperty, 302 | BBU_PROPERTIES_OT_RemoveProperty, 303 | BBU_PROPERTIES_OT_MoveProperty, 304 | BBUDataListItem, 305 | BBUPanel, 306 | ) 307 | 308 | 309 | def addon_path(): 310 | return os.path.dirname(os.path.realpath(__file__)) 311 | 312 | 313 | def load_shader(file_name): 314 | file = open(os.path.join(addon_path(), "shaders", file_name), 'r') 315 | data = file.read() 316 | file.close() 317 | 318 | return data 319 | 320 | 321 | vector3_shader = GPUShader(load_shader("vector3.vert"), load_shader("vector3.frag")) 322 | simple_color = GPUShader(load_shader("simple_color.vert"), load_shader("simple_color.frag")) 323 | 324 | draw_handler = None 325 | 326 | 327 | def draw(): 328 | obj = bpy.context.object 329 | if obj is None: 330 | return 331 | if not hasattr(obj, "bbu_visualization"): 332 | return 333 | if not obj.bbu_visualization: 334 | return 335 | if not hasattr(obj, "bbu_properties"): 336 | return 337 | 338 | properties = obj.bbu_properties 339 | 340 | vector3_pos = [] 341 | cuboid_pos = [] 342 | cuboid_indices = [] 343 | sphere_pos = [] 344 | capsule_pos = [] 345 | 346 | def parse_item(item): 347 | if item.type == "vector3": 348 | vector3_pos.append((item.vector3_x, item.vector3_y, item.vector3_z)) 349 | elif item.type == "cuboid": 350 | hx = item.cuboid_x 351 | hy = item.cuboid_y 352 | hz = item.cuboid_z 353 | 354 | # offsets 355 | ox = item.offset_x 356 | oy = item.offset_y 357 | oz = item.offset_z 358 | 359 | xn = ox - hx 360 | xp = ox + hx 361 | yn = oy - hy 362 | yp = oy + hy 363 | zn = oz - hz 364 | zp = oz + hz 365 | 366 | index = len(cuboid_pos) 367 | cuboid_pos.extend(( 368 | (xn, yn, zn), (xp, yn, zn), 369 | (xn, yp, zn), (xp, yp, zn), 370 | (xn, yn, zp), (xp, yn, zp), 371 | (xn, yp, zp), (xp, yp, zp), 372 | )) 373 | cuboid_indices.extend(( 374 | (index + 0, index + 1), (index + 0, index + 2), (index + 1, index + 3), (index + 2, index + 3), 375 | (index + 4, index + 5), (index + 4, index + 6), (index + 5, index + 7), (index + 6, index + 7), 376 | (index + 0, index + 4), (index + 1, index + 5), (index + 2, index + 6), (index + 3, index + 7), 377 | )) 378 | elif item.type == "sphere": 379 | radius = item.radius 380 | 381 | # offsets 382 | ox = item.offset_x 383 | oy = item.offset_y 384 | oz = item.offset_z 385 | 386 | segments = 60 387 | deg = 360.0 / segments 388 | 389 | last_deg = 0 390 | next_deg = deg 391 | for _ in range(segments): 392 | ra = math.radians(last_deg) 393 | rb = math.radians(next_deg) 394 | 395 | a1 = math.sin(ra) * radius 396 | a2 = math.cos(ra) * radius 397 | b1 = math.sin(rb) * radius 398 | b2 = math.cos(rb) * radius 399 | 400 | sphere_pos.extend(( 401 | (ox + a1, oy , oz + a2), 402 | (ox + b1, oy , oz + b2), 403 | (ox , oy + a1, oz + a2), 404 | (ox , oy + b1, oz + b2), 405 | (ox + a1, oy + a2, oz ), 406 | (ox + b1, oy + b2, oz ), 407 | )) 408 | 409 | last_deg += deg 410 | next_deg += deg 411 | elif item.type == "capsule": 412 | radius = item.radius 413 | height = item.height * 2.0 414 | up_direction = item.up_vector 415 | 416 | # offsets 417 | ox = item.offset_x 418 | oy = item.offset_y 419 | oz = item.offset_z 420 | 421 | segments = 60 422 | deg = 360.0 / segments 423 | 424 | last_deg = 0 425 | next_deg = deg 426 | 427 | d = height * 0.5 - radius 428 | 429 | for _ in range(segments): 430 | ra = math.radians(last_deg) 431 | rb = math.radians(next_deg) 432 | 433 | a1 = math.sin(ra) * radius 434 | a2 = math.cos(ra) * radius 435 | b1 = math.sin(rb) * radius 436 | b2 = math.cos(rb) * radius 437 | 438 | if up_direction == "zp": 439 | capsule_pos.extend(( 440 | (ox + a1, oy + a2, oz + d), 441 | (ox + b1, oy + b2, oz + d), 442 | (ox + a1, oy + a2, oz - d), 443 | (ox + b1, oy + b2, oz - d), 444 | )) 445 | if last_deg % 90.0 == 0.0: 446 | capsule_pos.extend(( 447 | (ox + a1, oy + a2, oz + d), 448 | (ox + a1, oy + a2, oz - d), 449 | )) 450 | 451 | dud = d if last_deg < 180.0 else -d 452 | 453 | capsule_pos.extend(( 454 | (ox , oy + a2, oz + dud + a1), 455 | (ox , oy + b2, oz + dud + b1), 456 | (ox + a2, oy , oz + dud + a1), 457 | (ox + b2, oy , oz + dud + b1), 458 | )) 459 | elif up_direction == "yp": 460 | capsule_pos.extend(( 461 | (ox + a1, oy + d, oz + a2), 462 | (ox + b1, oy + d, oz + b2), 463 | (ox + a1, oy - d, oz + a2), 464 | (ox + b1, oy - d, oz + b2), 465 | )) 466 | 467 | if last_deg % 90.0 == 0.0: 468 | capsule_pos.extend(( 469 | (ox + a1, oy + d, oz + a2), 470 | (ox + a1, oy - d, oz + a2), 471 | )) 472 | 473 | dud = d if last_deg < 180.0 else -d 474 | 475 | capsule_pos.extend(( 476 | (ox , oy + dud + a1, oz + a2), 477 | (ox , oy + dud + b1, oz + b2), 478 | (ox + a2, oy + dud + a1, oz ), 479 | (ox + b2, oy + dud + b1, oz ), 480 | )) 481 | elif up_direction == "xp": 482 | capsule_pos.extend(( 483 | (ox + d, oy + a1, oz + a2), 484 | (ox + d, oy + b1, oz + b2), 485 | (ox - d, oy + a1, oz + a2), 486 | (ox - d, oy + b1, oz + b2), 487 | )) 488 | 489 | if last_deg % 90.0 == 0.0: 490 | capsule_pos.extend(( 491 | (ox + d, oy + a1, oz + a2), 492 | (ox - d, oy + a1, oz + a2), 493 | )) 494 | 495 | dud = d if last_deg < 180.0 else -d 496 | 497 | capsule_pos.extend(( 498 | (ox + dud + a1, oy , oz + a2), 499 | (ox + dud + b1, oy , oz + b2), 500 | (ox + dud + a1, oy + a2, oz ), 501 | (ox + dud + b1, oy + b2, oz ), 502 | )) 503 | 504 | last_deg += deg 505 | next_deg += deg 506 | 507 | if obj.bbu_visualization_show_all: 508 | for item in properties: 509 | parse_item(item) 510 | else: 511 | parse_item(properties[obj.bbu_properties_index]) 512 | 513 | transform = obj.matrix_world; 514 | projection = bpy.context.region_data.perspective_matrix 515 | 516 | def draw_vector3(): 517 | if not vector3_pos: 518 | return 519 | 520 | vector3_shader.uniform_float("transform", transform) 521 | vector3_shader.uniform_float("projection", projection) 522 | vector3_shader.uniform_float("color", (0.6, 0.0, 0.8, 1.0)) 523 | batch = batch_for_shader(vector3_shader, "POINTS", {"pos": vector3_pos}) 524 | gpu.state.depth_test_set('LESS_EQUAL') 525 | gpu.state.depth_mask_set(True) 526 | batch.draw(vector3_shader) 527 | gpu.state.depth_mask_set(False) 528 | 529 | def draw_cuboid(): 530 | if not cuboid_pos: 531 | return 532 | 533 | simple_color.uniform_float("transform", transform) 534 | simple_color.uniform_float("projection", projection) 535 | simple_color.uniform_float("color", (0.4, 0.4, 0.8, 1.0)) 536 | batch = batch_for_shader(simple_color, "LINES", {"pos": cuboid_pos}, indices=cuboid_indices) 537 | gpu.state.depth_test_set('LESS_EQUAL') 538 | gpu.state.depth_mask_set(True) 539 | batch.draw(simple_color) 540 | gpu.state.depth_mask_set(False) 541 | 542 | def draw_sphere(): 543 | if not sphere_pos: 544 | return 545 | simple_color.uniform_float("transform", transform) 546 | simple_color.uniform_float("projection", projection) 547 | simple_color.uniform_float("color", (0.8, 0.2, 0.2, 1.0)) 548 | batch = batch_for_shader(simple_color, "LINES", {"pos": sphere_pos}) 549 | gpu.state.depth_test_set('LESS_EQUAL') 550 | gpu.state.depth_mask_set(True) 551 | batch.draw(simple_color) 552 | gpu.state.depth_mask_set(False) 553 | 554 | def draw_capsule(): 555 | if not capsule_pos: 556 | return 557 | 558 | simple_color.uniform_float("transform", transform) 559 | simple_color.uniform_float("projection", projection) 560 | simple_color.uniform_float("color", (0.2, 0.8, 0.2, 1.0)) 561 | batch = batch_for_shader(simple_color, "LINES", {"pos": capsule_pos}) 562 | gpu.state.depth_test_set('LESS_EQUAL') 563 | gpu.state.depth_mask_set(True) 564 | batch.draw(simple_color) 565 | gpu.state.depth_mask_set(False) 566 | 567 | draw_vector3() 568 | draw_cuboid() 569 | draw_sphere() 570 | draw_capsule() 571 | 572 | 573 | class glTF2ExportUserExtension: 574 | def __init__(self): 575 | from io_scene_gltf2.io.com.gltf2_io_extensions import Extension 576 | self.Extension = Extension 577 | 578 | def gather_node_hook(self, gltf2_object, _blender_object, _export_settings): 579 | if gltf2_object.extras is None: 580 | return 581 | if "bbu_properties" not in gltf2_object.extras: 582 | return 583 | 584 | properties = gltf2_object.extras["bbu_properties"] 585 | 586 | parsed = {} 587 | 588 | for item in properties: 589 | def get_or_default(name): 590 | return item[name] if name in item else defaults[name] 591 | 592 | def get_or_default_enum(lookup, name): 593 | return lookup[item[name]][0] if name in item else defaults[name] 594 | 595 | def get_vector(name): 596 | x = get_or_default("{}_x".format(name)) 597 | y = get_or_default("{}_y".format(name)) 598 | z = get_or_default("{}_z".format(name)) 599 | 600 | # convert to bevy coordinate system 601 | return (x, z, y) 602 | 603 | id = get_or_default("id") 604 | 605 | if id == "": 606 | continue 607 | if id in parsed: 608 | continue 609 | 610 | item_type = get_or_default_enum(item_types, "type") 611 | 612 | if item_type == "string": 613 | parsed[id] = get_or_default("string") 614 | elif item_type == "bool": 615 | parsed[id] = get_or_default("bool") 616 | elif item_type == "integer": 617 | parsed[id] = get_or_default("integer") 618 | elif item_type == "float": 619 | parsed[id] = get_or_default("float") 620 | elif item_type == "vector3": 621 | parsed[id] = get_vector("vector3") 622 | elif item_type == "cuboid": 623 | parsed[id] = { 624 | "cuboid": get_vector("cuboid"), 625 | "offset": get_vector("offset"), 626 | } 627 | elif item_type == "sphere": 628 | parsed[id] = { 629 | "radius": get_or_default("radius"), 630 | "offset": get_vector("offset"), 631 | } 632 | elif item_type == "capsule": 633 | up_vector = get_or_default_enum("up_vector") 634 | bevy_up_vector = None 635 | 636 | if up_vector == "xp": 637 | bevy_up_vector = (1, 0, 0) 638 | elif up_vector == "yp": 639 | bevy_up_vector = (0, 0, 1) 640 | elif up_vector == "zp": 641 | bevy_up_vector = (0, 1, 0) 642 | 643 | parsed[id] = { 644 | "radius": get_or_default("radius"), 645 | "height": get_or_default("height"), 646 | "offset": get_vector("offset"), 647 | "up_vector": bevy_up_vector, 648 | } 649 | 650 | del_if_exists(gltf2_object.extras, "bbu_properties") 651 | del_if_exists(gltf2_object.extras, "bbu_properties_index") 652 | del_if_exists(gltf2_object.extras, "bbu_visualization") 653 | del_if_exists(gltf2_object.extras, "bbu_visualization_show_all") 654 | 655 | gltf2_object.extras["bbu_object_data"] = parsed 656 | 657 | 658 | def register(): 659 | from bpy.utils import register_class 660 | for cls in classes: 661 | register_class(cls) 662 | 663 | bpy.types.Object.bbu_properties = bpy.props.CollectionProperty(type=BBUDataListItem) 664 | bpy.types.Object.bbu_properties_index = bpy.props.IntProperty(name="Property Index", default=0) 665 | bpy.types.Object.bbu_visualization = bpy.props.BoolProperty(default=True) 666 | bpy.types.Object.bbu_visualization_show_all = bpy.props.BoolProperty(default=True) 667 | 668 | global draw_handler 669 | draw_handler = bpy.types.SpaceView3D.draw_handler_add(draw, (), "WINDOW", "POST_VIEW") 670 | 671 | 672 | def unregister(): 673 | bpy.types.SpaceView3D.draw_handler_remove(draw_handler, "WINDOW") 674 | del bpy.types.Object.bbu_properties 675 | del bpy.types.Object.bbu_properties_index 676 | del bpy.types.Object.bbu_visualization 677 | del bpy.types.Object.bbu_visualization_show_all 678 | 679 | from bpy.utils import unregister_class 680 | for cls in reversed(classes): 681 | unregister_class(cls) 682 | 683 | 684 | if __name__ == "__main__": 685 | register() 686 | -------------------------------------------------------------------------------- /bevy_blender_utils/shaders/simple_color.frag: -------------------------------------------------------------------------------- 1 | uniform vec4 color; 2 | 3 | out vec4 fragColor; 4 | 5 | void main() { 6 | fragColor = color; 7 | } -------------------------------------------------------------------------------- /bevy_blender_utils/shaders/simple_color.vert: -------------------------------------------------------------------------------- 1 | uniform mat4 transform; 2 | uniform mat4 projection; 3 | 4 | in vec3 pos; 5 | 6 | void main() { 7 | gl_Position = projection * transform * vec4(pos, 1.0); 8 | } 9 | -------------------------------------------------------------------------------- /bevy_blender_utils/shaders/vector3.frag: -------------------------------------------------------------------------------- 1 | uniform vec4 color; 2 | 3 | out vec4 fragColor; 4 | 5 | void main() { 6 | vec2 coord = 2.0 * gl_PointCoord - 1.0; 7 | float radius = length(coord); 8 | 9 | float delta = fwidth(radius); 10 | float alpha = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, radius); 11 | 12 | fragColor = color * alpha; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /bevy_blender_utils/shaders/vector3.vert: -------------------------------------------------------------------------------- 1 | uniform mat4 transform; 2 | uniform mat4 projection; 3 | 4 | in vec3 pos; 5 | 6 | void main() { 7 | gl_Position = projection * transform * vec4(pos, 1.0); 8 | } 9 | -------------------------------------------------------------------------------- /examples/shooting_squares.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use bevy::prelude::*; 3 | use bevy::prelude::shape::Cube; 4 | use serde::Deserialize; 5 | use bevy_blender_utils::{BBUManager, BBUPlugin, BBUSceneSpawnedEventWithId, SceneId}; 6 | 7 | fn main() { 8 | App::new() 9 | .add_plugins(DefaultPlugins) 10 | // SceneId can be () unit-type if no identifier is needed. 11 | .add_plugins(BBUPlugin::::default()) 12 | // Need to register type as scene requires it. I'm not sure if this has any implication 13 | // in this case. 14 | .register_type::() 15 | .add_systems(Startup, (build_camera_and_lights, load_assets)) 16 | .add_systems(Update, (spawn_bbu_assets, periodic_spawn, move_squares)) 17 | .run(); 18 | } 19 | 20 | // Parsed data, the format isn't opinionated; this is just how I prefer to use it. 21 | // It is parsed from JSON like this: 22 | // { "id": "shooting_square", "spawn_point": [0.0, 1.0, 0.0], "speed": 0.75 } 23 | #[derive(Deserialize, Debug)] 24 | #[serde(rename_all="snake_case", tag = "id")] 25 | enum SceneObjects { 26 | ShootingSquare { 27 | spawn_point: Vec3, 28 | speed: f32, 29 | } 30 | } 31 | 32 | // Creating a scene id isn't required for this example. Still, it can categorize different scenes 33 | // and have id-dependent logic on the same data. 34 | #[derive(Copy, Clone)] 35 | enum Scenes { 36 | MainScene 37 | } 38 | 39 | impl SceneId for Scenes {} 40 | 41 | fn build_camera_and_lights(mut commands: Commands) { 42 | commands.spawn(Camera3dBundle { 43 | transform: Transform::from_translation(Vec3::new(0.0, 2.0, 4.0)) 44 | .looking_at(Vec3::ZERO, Vec3::Y), 45 | ..default() 46 | }); 47 | commands.spawn(SpotLightBundle { 48 | transform: Transform::from_translation(Vec3::new(0.0, 3.0, 5.0)) 49 | .looking_at(Vec3::ZERO, Vec3::Y), 50 | spot_light: SpotLight { 51 | range: 20.0, 52 | ..default() 53 | }, 54 | ..default() 55 | }); 56 | } 57 | 58 | fn load_assets( 59 | mut bbu_manager: ResMut>, 60 | asset_server: Res, 61 | ) { 62 | // Add a handle to our scene with the id we want to use. Id can be () unit-type. 63 | // .blend file and .gltf file can also be found on /assets. 64 | bbu_manager.manage(Scenes::MainScene, asset_server.load("shooting_squares.glb#Scene0")); 65 | } 66 | 67 | // The component that we are going to insert into the scene. It needs to derive reflect because of 68 | // the Scene. 69 | #[derive(Component, Reflect, Default)] 70 | #[reflect(Component)] 71 | struct ShootingSquare { 72 | spawn_point: Vec3, 73 | speed: f32, 74 | } 75 | 76 | #[derive(Component)] 77 | struct ShootSpeed(f32); 78 | #[derive(Component)] 79 | struct SpawnedSquare; 80 | 81 | // We get events for each scene that's loaded. We can do this from multiple systems because of how 82 | // events work. But realistically speaking, it isn't good performance-wise, and it doesn't have any 83 | // advantages that I know of. 84 | 85 | fn spawn_bbu_assets( 86 | mut commands: Commands, 87 | mut reader: EventReader>, 88 | mut scenes: ResMut>, 89 | ) { 90 | for event in reader.iter() { 91 | // Parsing scenes and error handling. This skips error handling for the sake of the example. 92 | let Ok(mut scene) = event.parse(&mut scenes) else { continue; }; 93 | let scene = &mut scene; 94 | 95 | match scene.id { 96 | Scenes::MainScene => scene.parse::(|commands, entity, _name, data| { 97 | // More error handling. 98 | let Ok(data) = data else { return; }; 99 | // This isn't an error and can be used because not all scenes require extra data. 100 | // It can be used for handling other objects that have no data. 101 | let Some(data) = data else { return; }; 102 | 103 | match data { 104 | SceneObjects::ShootingSquare { spawn_point, speed } => { 105 | commands 106 | .entity(entity) 107 | .insert(ShootingSquare { 108 | spawn_point, 109 | speed, 110 | }); 111 | } 112 | } 113 | }), 114 | } 115 | 116 | // We updated our scene world; now time to spawn it. We could also save the handle of it and 117 | // spawn later, for stuff like loading screens. 118 | commands.spawn(SceneBundle { 119 | scene: scene.handle.clone(), 120 | ..default() 121 | }); 122 | } 123 | } 124 | 125 | struct EachSecondTimer(Timer); 126 | 127 | impl Default for EachSecondTimer { 128 | fn default() -> Self { 129 | Self(Timer::new(Duration::from_secs(1), TimerMode::Repeating)) 130 | } 131 | } 132 | 133 | fn periodic_spawn( 134 | mut commands: Commands, 135 | query: Query<(&GlobalTransform, &ShootingSquare)>, 136 | time: Res