├── .gitignore ├── LICENSE ├── README.md ├── addons └── better-texture-array │ ├── icons │ ├── ALL.svg │ ├── ALL.svg.import │ ├── ALPHA.svg │ ├── ALPHA.svg.import │ ├── BLUE.svg │ ├── BLUE.svg.import │ ├── GREEN.svg │ ├── GREEN.svg.import │ ├── RED.svg │ └── RED.svg.import │ ├── plugin.cfg │ ├── plugin.gd │ ├── texture_array_builder.gd │ ├── texture_array_inspector.gd │ ├── texture_array_saver.gd │ └── ui │ ├── create_dialog.tscn │ ├── edit_dialog.gd │ ├── editor.gd │ ├── layer.gd │ └── layer.shader ├── icon.png ├── project.godot └── screenshots ├── .gdignore └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot-specific ignores 2 | .import/ 3 | export.cfg 4 | export_presets.cfg 5 | 6 | # Imported translations (automatically generated from CSV files) 7 | *.translation 8 | 9 | # Mono-specific ignores 10 | .mono/ 11 | data_*/ 12 | 13 | # Binaries 14 | bin/ 15 | build/ 16 | lib/ 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2020 Adrien de Pierres 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [screenshot]: https://raw.githubusercontent.com/awkwardpolygons/better-texture-array/master/screenshots/screenshot.png "BetterTextureArray screenshot" 2 | # BetterTextureArray 3 | Adds support for saving, viewing and editing TextureArray and Texture3D in Godot 3. 4 | 5 | ![][screenshot] 6 | 7 | # Explanation 8 | Godot 3.x has very buggy behaviour when attempting to save a TextureLayered (TextureArray and Texture3D), it also has poor support for TextureLayered (TextureArray and Texture3D) in the editor. 9 | 10 | Attempting to save TextureArray and Texture3D inside the scene or as `.res` or `.tres`, produces errors. 11 | This plugin adds support for saving TextureArray and Texture3D as `.texarr` and `.tex3d` files both in the editor and from code. 12 | These are the native Godot importer formats for TextureArray and Texture3D. 13 | 14 | This plugin also adds UI so that you can create, view, edit and, save TextureArray and Texture3D in the editor. 15 | The editor UI allows you to edit individual layers, and each RGBA channel on a layer as well. This is very useful when trying to merge multiple PBR material textures into one file. 16 | 17 | ## Extras 18 | The plugin also provides a custom importer for TextureArray and Texture3D. 19 | 20 | The builtin importer requires a large single image with each of the layers as a tile in a grid. 21 | 22 | With the BetterTextureArray importer, you can use a simple `JSON` build format to create TextureArray and Texture3D from a list of other image files. 23 | 24 | # Guide 25 | ## Installation 26 | 1. To install copy the `better-texture-array` from the `addons` folder to your project's `addons` folder. 27 | 2. Open your project in the edtior and choose Project Settings... -> Plugins from the Project menu and enable BetterTextureArray. 28 | 29 | ## Editor 30 | 1. To create a new TextureArray or Texture3D click the create new resource icon in the inspector. 31 | 2. Then click the **create** button in the new resource to set the width, height and depth (the number of layers), and choose the image format. 32 | 3. Click the **show** toggle to show or hide the layers. 33 | 4. You should have several new white layers. 34 | 5. **Double click** a layer to set an image for that layer. 35 | 6. Select a channel from the channel buttons to view only that channel for the layers. 36 | 7. With a channel selected, **double click** a layer to set data for that channel only. The file chooser in channel mode will include a drop down to choose the source channel from your selected file. 37 | 8. You can select a layer by **clicking** on it. The index of the selected layer will be set in the metadata of the TextureArray or Texture3D as `layer_selected` and the `changed` signal will be emitted. This is useful if you want to interact with this UI in another editor plugin. 38 | 9. When saving the TextureArray or Texture3D from the Editor, you must choose to save as either a `.texarr` or `.tex3d` file, **DO NOT** save as `.res` or `.tres`, these will error. Also **DO NOT** save the TextureArray or Texture3D in your scene, this will error and bloat your scene file. 39 | 40 | ## Importer 41 | Create a `JSON` file with a `.ta-builder` file extension for TextureArray and `.t3d-builder` extension for Texture3D. 42 | Select the file from the importer in the editor to customize import properties. 43 | 44 | The `JSON` object must contain a `size` array property, and a `layers` array property. 45 | 46 | The `size` property must have two values for the width and height the layers. 47 | 48 | The `layers` will contain URLs to each image file or resource to use for each layer. You can use a `res://` or a `file://` reference. 49 | 50 | A simple example file: 51 | ```json 52 | { 53 | "size": [1024, 1024], 54 | "layers": [ 55 | "res://assets/textures/rock1.jpg", 56 | "res://assets/textures/rock2.jpg", 57 | "res://assets/textures/rock3.jpg", 58 | "file://home/user/assets/snow1.jpg", 59 | ], 60 | } 61 | ``` 62 | 63 | The `layers` property can also contain a more complex blending object instead of a simple URL if you want to mix channels from multiple sources into one layer. 64 | 65 | A more complex channel mixing example: 66 | ```json 67 | { 68 | "size": [1024, 1024], 69 | "layers": [ 70 | { 71 | "rgb": ["res://assets/textures/albedo.jpg", "rgb"], 72 | "a": ["res://assets/textures/bump.jpg", "r"], 73 | }, 74 | ], 75 | } 76 | ``` 77 | 78 | The blending object property keys are the target channels, represented by `r`, `g`, `b`, and `a`, on their own or in combination. 79 | 80 | The blending object property value is an array of two values, the first is the source file or resource, the second is string with the source channels or channel, again represented by `r`, `g`, `b`, and `a`. 81 | -------------------------------------------------------------------------------- /addons/better-texture-array/icons/ALL.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /addons/better-texture-array/icons/ALL.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/ALL.svg-c5bb36e223db668f702b17531d71962b.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://addons/better-texture-array/icons/ALL.svg" 13 | dest_files=[ "res://.import/ALL.svg-c5bb36e223db668f702b17531d71962b.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /addons/better-texture-array/icons/ALPHA.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /addons/better-texture-array/icons/ALPHA.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/ALPHA.svg-6e19ad813341a72d977844c24ac25c0e.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://addons/better-texture-array/icons/ALPHA.svg" 13 | dest_files=[ "res://.import/ALPHA.svg-6e19ad813341a72d977844c24ac25c0e.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /addons/better-texture-array/icons/BLUE.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /addons/better-texture-array/icons/BLUE.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/BLUE.svg-92ce476922e07f3d7a41c1565648292b.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://addons/better-texture-array/icons/BLUE.svg" 13 | dest_files=[ "res://.import/BLUE.svg-92ce476922e07f3d7a41c1565648292b.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /addons/better-texture-array/icons/GREEN.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /addons/better-texture-array/icons/GREEN.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/GREEN.svg-d66a53277a76362b9be4f443ff6131e0.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://addons/better-texture-array/icons/GREEN.svg" 13 | dest_files=[ "res://.import/GREEN.svg-d66a53277a76362b9be4f443ff6131e0.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /addons/better-texture-array/icons/RED.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /addons/better-texture-array/icons/RED.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/RED.svg-c593494ea31a14a1ece5d958fd3dff72.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://addons/better-texture-array/icons/RED.svg" 13 | dest_files=[ "res://.import/RED.svg-c593494ea31a14a1ece5d958fd3dff72.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /addons/better-texture-array/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="BetterTextureArray" 4 | description="Adds support for saving, viewing and editing TextureArray and Texture3D in Godot 3. 5 | Also provides a better importer that creates TextureArray or Texure3D from a JSON build file." 6 | author="Adrien de Pierres" 7 | version="1.0.0" 8 | script="plugin.gd" 9 | -------------------------------------------------------------------------------- /addons/better-texture-array/plugin.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorPlugin 3 | 4 | var editor: EditorInterface 5 | var undo_redo: UndoRedo 6 | var filesystem: EditorFileSystem 7 | var import_array 8 | var import_3d 9 | var inspect_array 10 | 11 | func _enter_tree(): 12 | editor = get_editor_interface() 13 | undo_redo = get_undo_redo() 14 | filesystem = editor.get_resource_filesystem() 15 | import_array = preload("res://addons/better-texture-array/texture_array_builder.gd").new() 16 | import_array.is_3d = false 17 | import_3d = preload("res://addons/better-texture-array/texture_array_builder.gd").new() 18 | import_3d.is_3d = true 19 | add_import_plugin(import_array) 20 | add_import_plugin(import_3d) 21 | inspect_array = preload("res://addons/better-texture-array/texture_array_inspector.gd").new() 22 | inspect_array.undo_redo = undo_redo 23 | add_inspector_plugin(inspect_array) 24 | 25 | func _exit_tree(): 26 | remove_import_plugin(import_array) 27 | remove_import_plugin(import_3d) 28 | remove_inspector_plugin(inspect_array) 29 | -------------------------------------------------------------------------------- /addons/better-texture-array/texture_array_builder.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorImportPlugin 3 | 4 | enum Presets { DEFAULT } 5 | enum Compress { LOSSLESS, VRAM, UNCOMPRESSED } 6 | enum Format { FORMAT_L8, FORMAT_LA8, FORMAT_R8, FORMAT_RG8, FORMAT_RGB8, FORMAT_RGBA8 } 7 | const formats = { 8 | "FORMAT_L8": Image.FORMAT_L8, 9 | "FORMAT_LA8": Image.FORMAT_LA8, 10 | "FORMAT_R8": Image.FORMAT_R8, 11 | "FORMAT_RG8": Image.FORMAT_RG8, 12 | "FORMAT_RGB8": Image.FORMAT_RGB8, 13 | "FORMAT_RGBA8": Image.FORMAT_RGBA8, 14 | } 15 | 16 | var is_3d: bool = false 17 | 18 | func get_importer_name(): 19 | return "texture_3d_builder" if is_3d else "texture_array_builder" 20 | 21 | func get_visible_name(): 22 | return "Texture3DBuilder" if is_3d else "TextureArrayBuilder" 23 | 24 | func get_recognized_extensions(): 25 | return ["ta-builder", "t3d-builder"] 26 | 27 | func get_save_extension(): 28 | return "tex3d" if is_3d else "texarr" 29 | 30 | func get_resource_type(): 31 | return "Texture3D" if is_3d else "TextureArray" 32 | 33 | func get_preset_count(): 34 | return Presets.size() 35 | 36 | func get_preset_name(preset): 37 | return "" 38 | 39 | func get_import_options(preset): 40 | match preset: 41 | Presets.DEFAULT: 42 | return [ 43 | { 44 | "name": "format", 45 | "default_value": Format.FORMAT_RGBA8, 46 | "property_hint": PROPERTY_HINT_ENUM, 47 | "hint_string": "FORMAT_L8,FORMAT_LA8,FORMAT_R8,FORMAT_RG8,FORMAT_RGB8,FORMAT_RGBA8" 48 | }, 49 | # { 50 | # "name": "compress", 51 | # "default_value": null, 52 | # "hint_string": "compress/", 53 | # "usage": PROPERTY_USAGE_GROUP | PROPERTY_USAGE_CATEGORY, 54 | # }, 55 | { 56 | "name": "compress", 57 | "default_value": Compress.VRAM, 58 | "property_hint": PROPERTY_HINT_ENUM, 59 | "hint_string": "Lossless,VRAM,Uncompressed" 60 | }, 61 | # { 62 | # "name": "flags", 63 | # "default_value": null, 64 | # "hint_string": "flags/", 65 | # "usage": PROPERTY_USAGE_GROUP | PROPERTY_USAGE_CATEGORY, 66 | # }, 67 | { 68 | "name": "flags", 69 | "default_value": 7, 70 | "property_hint": PROPERTY_HINT_FLAGS, 71 | "hint_string": "Mipmaps,Repeat,Filter" 72 | }, 73 | ] 74 | _: 75 | return [] 76 | 77 | func get_option_visibility(option, options): 78 | return true 79 | 80 | func import(source_file, save_path, options, r_platform_variants, r_gen_files): 81 | var file = File.new() 82 | var err = file.open(source_file, File.READ) 83 | if err != OK: 84 | file.close() 85 | return err 86 | var json = file.get_as_text() 87 | file.close() 88 | var parsed = JSON.parse(json) 89 | if parsed.error != OK: 90 | return parsed.error 91 | var obj = parsed.result 92 | 93 | var format = options["format"] 94 | var compress = options["compress"] 95 | var flags = options["flags"] 96 | var images = [] 97 | obj.format = format 98 | 99 | images = _parse(obj) 100 | 101 | if compress == Compress.VRAM: 102 | err = _save_tex_vram(images, save_path, flags, r_platform_variants) 103 | else: 104 | err = _save_tex(images, "%s.%s" % [save_path, get_save_extension()], compress, -1, flags) 105 | prints("TextureArray %s built and saved" % [source_file]) 106 | return err 107 | 108 | func _parse(obj): 109 | assert(obj.size is Array and len(obj.size) == 2, "Invalid size, must be an array of two ints: %s" % [obj.size]) 110 | var size = Vector2(obj.size[0], obj.size[1]) 111 | var format = obj.format 112 | var images = [] 113 | 114 | for layer in obj.layers: 115 | var img: Image = null 116 | if layer is String: 117 | img = _load_image(layer, size) 118 | elif layer is Dictionary: 119 | img = _get_image_from_channels(layer, size, format) 120 | images.append(img) 121 | 122 | return images 123 | 124 | func _get_image_from_channels(channels, size: Vector2, format: int = Image.FORMAT_RGBA8): 125 | var dst_img = Image.new() 126 | dst_img.create(size.x, size.y, false, format) 127 | 128 | for dst in channels: 129 | assert(channels[dst] is Array and len(channels[dst]) == 2 and channels[dst][0] is String and channels[dst][1] is String, "Invalid format for channels layer: %s" % [channels[dst]]) 130 | assert(len(dst) == len(channels[dst][1]), "Channel index length mismatch: %s, %s" % [dst, channels[dst]]) 131 | var src_img = _load_image(channels[dst][0], size) 132 | var src = channels[dst][1] 133 | src_img.lock() 134 | dst_img.lock() 135 | for y in size.y: 136 | for x in size.x: 137 | var dst_px = dst_img.get_pixel(x, y) 138 | var src_px = src_img.get_pixel(x, y) 139 | for i in len(dst): 140 | var dst_ch = dst[i] 141 | var src_ch = src[i] 142 | dst_px[dst_ch] = src_px[src_ch] 143 | dst_img.set_pixel(x, y, dst_px) 144 | dst_img.unlock() 145 | src_img.unlock() 146 | 147 | return dst_img 148 | 149 | func _load_image(path: String, size: Vector2, format: int = Image.FORMAT_RGBA8) -> Image: 150 | var img 151 | if path.begins_with("#"): 152 | img = Image.new() 153 | img.create(size.x, size.y, false, format) 154 | img.fill(Color(path)) 155 | elif path.begins_with("file://"): 156 | img = Image.new() 157 | img.load(path.substr(5)) 158 | else: 159 | img = load(path) 160 | if img is Texture: 161 | img = img.get_data() 162 | img.decompress() 163 | img.convert(format) 164 | img.resize(size.x, size.y) 165 | return img 166 | 167 | func _save_tex_vram(images: Array, path: String, flags: int, r_comp: Array): 168 | if ProjectSettings.get("rendering/vram_compression/import_bptc"): 169 | r_comp.append("bptc") 170 | _save_tex(images, "%s.%s.%s" % [path, "bptc", get_save_extension()], Compress.VRAM, Image.COMPRESS_BPTC, flags) 171 | if ProjectSettings.get("rendering/vram_compression/import_s3tc"): 172 | r_comp.append("s3tc") 173 | _save_tex(images, "%s.%s.%s" % [path, "s3tc", get_save_extension()], Compress.VRAM, Image.COMPRESS_S3TC, flags) 174 | if ProjectSettings.get("rendering/vram_compression/import_etc2"): 175 | r_comp.append("etc2") 176 | _save_tex(images, "%s.%s.%s" % [path, "etc2", get_save_extension()], Compress.VRAM, Image.COMPRESS_ETC2, flags) 177 | if ProjectSettings.get("rendering/vram_compression/import_etc"): 178 | r_comp.append("etc") 179 | _save_tex(images, "%s.%s.%s" % [path, "etc", get_save_extension()], Compress.VRAM, Image.COMPRESS_ETC, flags) 180 | if ProjectSettings.get("rendering/vram_compression/import_pvrtc"): 181 | r_comp.append("pvrtc") 182 | _save_tex(images, "%s.%s.%s" % [path, "pvrtc", get_save_extension()], Compress.VRAM, Image.COMPRESS_PVRTC4, flags) 183 | 184 | func _save_tex(images: Array, path: String, compression: int = Compress.UNCOMPRESSED, vram_compression: int = Image.COMPRESS_S3TC, flags: int = 7): 185 | var num_images = images.size() 186 | 187 | var file = File.new() 188 | var err = file.open(path, File.WRITE) 189 | if err != 0: 190 | return err 191 | 192 | file.store_8(ord('G')) 193 | file.store_8(ord('D')) 194 | file.store_8(ord('3' if is_3d else 'A')) # Godot Texture3D or TextureArray 195 | file.store_8(ord('T')) # Godot streamable texture 196 | 197 | file.store_32(images[0].get_width() if num_images > 0 else 0) 198 | file.store_32(images[0].get_height() if num_images > 0 else 0) 199 | file.store_32(images.size()) 200 | file.store_32(flags) 201 | 202 | if num_images == 0: 203 | return 204 | 205 | if compression != Compress.VRAM: 206 | file.store_32(images[0].get_format()) 207 | file.store_32(compression) # Compression: 0 - LOSSLESS (PNG), 1 - vram, 2 - UNCOMPRESSED 208 | 209 | var dim = max(images[0].get_width(), images[0].get_height()) 210 | var mipmap_count = log(dim) / log(2) + 1 211 | 212 | for i in images.size(): 213 | var img = images[i].duplicate() as Image 214 | var data: PoolByteArray; 215 | 216 | if flags & TextureLayered.FLAG_MIPMAPS: 217 | img.generate_mipmaps() 218 | else: 219 | img.clear_mipmaps() 220 | 221 | match compression: 222 | Compress.LOSSLESS: 223 | file.store_32(mipmap_count) 224 | for j in mipmap_count: 225 | if j > 0: 226 | img.shrink_x2() 227 | data = img.save_png_to_buffer() 228 | file.store_32(data.size() + 4) 229 | file.store_8(ord('P')) 230 | file.store_8(ord('N')) 231 | file.store_8(ord('G')) 232 | file.store_8(ord(' ')) 233 | file.store_buffer(data) 234 | Compress.VRAM: 235 | img.compress(vram_compression, 3, 0.7) # COMPRESS_SOURCE_LAYERED = 3, not bound to GDScript? 236 | if i == 0: 237 | file.store_32(img.get_format()) 238 | file.store_32(compression) 239 | data = img.get_data() 240 | file.store_buffer(data) 241 | Compress.UNCOMPRESSED: 242 | data = img.get_data() 243 | file.store_buffer(data) 244 | 245 | file.close() 246 | return OK 247 | -------------------------------------------------------------------------------- /addons/better-texture-array/texture_array_inspector.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorInspectorPlugin 3 | 4 | const Editor = preload("res://addons/better-texture-array/ui/editor.gd") 5 | var undo_redo: UndoRedo 6 | 7 | func can_handle(object): 8 | return object is Texture3D or object is TextureArray 9 | 10 | func parse_property(object, type, path, hint, hint_text, usage): 11 | # Use the `flags` property as our anchor because there is no layers / data prop exported, 12 | # make sure not to return `true` at the end because that would remove the flags UI 13 | if path != "flags": 14 | return 15 | 16 | var ed = Editor.new() 17 | ed.undo_redo = undo_redo 18 | add_property_editor("data", ed) 19 | -------------------------------------------------------------------------------- /addons/better-texture-array/texture_array_saver.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends ResourceFormatSaver 3 | class_name TextureArraySaver 4 | 5 | func get_recognized_extensions(res: Resource) -> PoolStringArray: 6 | return PoolStringArray(["texarr", "tex3d"]) if recognize(res) else PoolStringArray() 7 | 8 | func recognize(res: Resource) -> bool: 9 | return res is TextureArray or res is Texture3D 10 | 11 | func save(path: String, res: Resource, flags: int) -> int: 12 | var imp = preload("res://addons/better-texture-array/texture_array_builder.gd").new() 13 | imp.is_3d = path.get_extension() == "tex3d" 14 | res.take_over_path(path) 15 | return imp._save_tex(res.data.layers, path, imp.Compress.LOSSLESS, -1, res.flags) 16 | -------------------------------------------------------------------------------- /addons/better-texture-array/ui/create_dialog.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [sub_resource type="GDScript" id=1] 4 | script/source = "tool 5 | extends WindowDialog 6 | 7 | var width_input 8 | var height_input 9 | var depth_input 10 | var format_input 11 | var compress_input 12 | var compress_vram_input 13 | var formats = { 14 | \"FORMAT_L8\": Image.FORMAT_L8, 15 | \"FORMAT_LA8\": Image.FORMAT_LA8, 16 | \"FORMAT_R8\": Image.FORMAT_R8, 17 | \"FORMAT_RG8\": Image.FORMAT_RG8, 18 | \"FORMAT_RGB8\": Image.FORMAT_RGB8, 19 | \"FORMAT_RGBA8\": Image.FORMAT_RGBA8, 20 | \"FORMAT_RGBH\": Image.FORMAT_RGBH, 21 | \"FORMAT_RGBAH\": Image.FORMAT_RGBAH, 22 | \"FORMAT_RGBF\": Image.FORMAT_RGBF, 23 | \"FORMAT_RGBAF\": Image.FORMAT_RGBAF, 24 | } 25 | 26 | var compression_vram = { 27 | \"NONE\": -1, 28 | \"COMPRESS_S3TC\": Image.COMPRESS_S3TC, 29 | \"COMPRESS_PVRTC2\": Image.COMPRESS_PVRTC2, 30 | \"COMPRESS_PVRTC4\": Image.COMPRESS_PVRTC4, 31 | \"COMPRESS_ETC\": Image.COMPRESS_ETC, 32 | \"COMPRESS_ETC2\": Image.COMPRESS_ETC2, 33 | } 34 | 35 | var compression = { 36 | \"Lossless\": 0, 37 | \"VRAM\": 1, 38 | \"Uncompressed\": 2 39 | } 40 | 41 | signal acknowledged 42 | 43 | func _ready(): 44 | width_input = $Box/WidthInput/Width 45 | height_input = $Box/HeightInput/Height 46 | depth_input = $Box/DepthInput/Depth 47 | format_input = $Box/FormatInput/Format 48 | compress_input = $Box/CompressInput/CompressMode 49 | compress_vram_input = $Box/CompressVRAMInput/CompressMode 50 | 51 | format_input.clear() 52 | for k in formats: 53 | var v = formats[k] 54 | format_input.add_item(k, v) 55 | format_input.selected = 5 56 | 57 | compress_input.clear() 58 | for k in compression: 59 | var v = compression[k] 60 | compress_input.add_item(k, v) 61 | 62 | compress_vram_input.clear() 63 | for k in compression_vram: 64 | var v = compression_vram[k] 65 | compress_vram_input.add_item(k, v) 66 | 67 | # call_deferred(\"popup_centered\") 68 | 69 | func _on_action(ok): 70 | var vals = [] 71 | if ok: 72 | var selected = format_input.selected 73 | var format = format_input.get_item_id(selected) if selected > -1 else -1 74 | selected = compress_input.selected 75 | var compress = compress_input.get_item_id(selected) if selected > -1 else -1 76 | selected = compress_vram_input.selected 77 | var compress_vram = compress_vram_input.get_item_id(selected) if selected > -1 else -1 78 | vals = [width_input.value, height_input.value, depth_input.value, format, Texture.FLAGS_DEFAULT, compress, compress_vram] 79 | emit_signal(\"acknowledged\", ok, vals) 80 | hide() 81 | 82 | func _on_compression_selected(i): 83 | $Box/CompressVRAMInput.visible = i == 1 84 | rect_size.y = rect_min_size.y + compress_vram_input.rect_size.y * int(i == 1) 85 | " 86 | 87 | [node name="CreateDialog" type="WindowDialog"] 88 | visible = true 89 | anchor_right = 0.1 90 | anchor_bottom = 0.05 91 | margin_right = 350.0 92 | margin_bottom = 200.0 93 | rect_min_size = Vector2( 350, 200 ) 94 | window_title = "Create layers..." 95 | resizable = true 96 | script = SubResource( 1 ) 97 | __meta__ = { 98 | "_edit_use_anchors_": false 99 | } 100 | 101 | [node name="Box" type="VBoxContainer" parent="."] 102 | anchor_right = 1.0 103 | anchor_bottom = 1.0 104 | margin_left = 8.0 105 | margin_top = 8.0 106 | margin_right = -8.0 107 | margin_bottom = -8.0 108 | __meta__ = { 109 | "_edit_use_anchors_": false 110 | } 111 | 112 | [node name="WidthInput" type="HBoxContainer" parent="Box"] 113 | margin_right = 436.0 114 | margin_bottom = 32.0 115 | rect_min_size = Vector2( 0, 32 ) 116 | 117 | [node name="Label" type="Label" parent="Box/WidthInput"] 118 | margin_top = 9.0 119 | margin_right = 216.0 120 | margin_bottom = 23.0 121 | size_flags_horizontal = 3 122 | text = "Width" 123 | 124 | [node name="Width" type="SpinBox" parent="Box/WidthInput"] 125 | margin_left = 220.0 126 | margin_right = 436.0 127 | margin_bottom = 32.0 128 | size_flags_horizontal = 3 129 | min_value = 32.0 130 | max_value = 4096.0 131 | value = 1024.0 132 | 133 | [node name="HeightInput" type="HBoxContainer" parent="Box"] 134 | margin_top = 36.0 135 | margin_right = 436.0 136 | margin_bottom = 68.0 137 | rect_min_size = Vector2( 0, 32 ) 138 | 139 | [node name="Label" type="Label" parent="Box/HeightInput"] 140 | margin_top = 9.0 141 | margin_right = 216.0 142 | margin_bottom = 23.0 143 | size_flags_horizontal = 3 144 | text = "Height" 145 | 146 | [node name="Height" type="SpinBox" parent="Box/HeightInput"] 147 | margin_left = 220.0 148 | margin_right = 436.0 149 | margin_bottom = 32.0 150 | size_flags_horizontal = 3 151 | min_value = 32.0 152 | max_value = 4096.0 153 | value = 1024.0 154 | 155 | [node name="DepthInput" type="HBoxContainer" parent="Box"] 156 | margin_top = 72.0 157 | margin_right = 436.0 158 | margin_bottom = 104.0 159 | rect_min_size = Vector2( 0, 32 ) 160 | 161 | [node name="Label" type="Label" parent="Box/DepthInput"] 162 | margin_top = 9.0 163 | margin_right = 216.0 164 | margin_bottom = 23.0 165 | size_flags_horizontal = 3 166 | text = "Depth" 167 | 168 | [node name="Depth" type="SpinBox" parent="Box/DepthInput"] 169 | margin_left = 220.0 170 | margin_right = 436.0 171 | margin_bottom = 32.0 172 | size_flags_horizontal = 3 173 | min_value = 1.0 174 | max_value = 16.0 175 | value = 16.0 176 | 177 | [node name="FormatInput" type="HBoxContainer" parent="Box"] 178 | margin_top = 108.0 179 | margin_right = 436.0 180 | margin_bottom = 140.0 181 | rect_min_size = Vector2( 0, 32 ) 182 | 183 | [node name="Label" type="Label" parent="Box/FormatInput"] 184 | margin_top = 9.0 185 | margin_right = 216.0 186 | margin_bottom = 23.0 187 | size_flags_horizontal = 3 188 | text = "Format" 189 | 190 | [node name="Format" type="OptionButton" parent="Box/FormatInput"] 191 | margin_left = 220.0 192 | margin_right = 436.0 193 | margin_bottom = 32.0 194 | size_flags_horizontal = 3 195 | text = "FORMAT_RGBA8" 196 | items = [ "FORMAT_L8", null, false, 0, null, "FORMAT_LA8", null, false, 1, null, "FORMAT_R8", null, false, 2, null, "FORMAT_RG8", null, false, 3, null, "FORMAT_RGB8", null, false, 4, null, "FORMAT_RGBA8", null, false, 5, null, "FORMAT_RGBH", null, false, 14, null, "FORMAT_RGBAH", null, false, 15, null, "FORMAT_RGBF", null, false, 10, null, "FORMAT_RGBAF", null, false, 11, null ] 197 | selected = 5 198 | 199 | [node name="CompressInput" type="HBoxContainer" parent="Box"] 200 | visible = false 201 | margin_top = 144.0 202 | margin_right = 436.0 203 | margin_bottom = 176.0 204 | rect_min_size = Vector2( 0, 32 ) 205 | 206 | [node name="Label" type="Label" parent="Box/CompressInput"] 207 | margin_top = 9.0 208 | margin_right = 216.0 209 | margin_bottom = 23.0 210 | size_flags_horizontal = 3 211 | text = "Compression" 212 | 213 | [node name="CompressMode" type="OptionButton" parent="Box/CompressInput"] 214 | margin_left = 220.0 215 | margin_right = 436.0 216 | margin_bottom = 32.0 217 | size_flags_horizontal = 3 218 | text = "Lossless" 219 | items = [ "Lossless", null, false, 0, null, "VRAM", null, false, 1, null, "Uncompressed", null, false, 2, null ] 220 | selected = 0 221 | 222 | [node name="CompressVRAMInput" type="HBoxContainer" parent="Box"] 223 | visible = false 224 | margin_top = 180.0 225 | margin_right = 436.0 226 | margin_bottom = 212.0 227 | rect_min_size = Vector2( 0, 32 ) 228 | 229 | [node name="Label" type="Label" parent="Box/CompressVRAMInput"] 230 | margin_top = 9.0 231 | margin_right = 216.0 232 | margin_bottom = 23.0 233 | size_flags_horizontal = 3 234 | text = "VRAM Compression" 235 | 236 | [node name="CompressMode" type="OptionButton" parent="Box/CompressVRAMInput"] 237 | margin_left = 220.0 238 | margin_right = 436.0 239 | margin_bottom = 32.0 240 | size_flags_horizontal = 3 241 | text = "NONE" 242 | items = [ "NONE", null, false, 0, null, "COMPRESS_S3TC", null, false, 0, null, "COMPRESS_PVRTC2", null, false, 1, null, "COMPRESS_PVRTC4", null, false, 2, null, "COMPRESS_ETC", null, false, 3, null, "COMPRESS_ETC2", null, false, 4, null ] 243 | selected = 0 244 | 245 | [node name="Spacer" type="Control" parent="Box"] 246 | margin_top = 144.0 247 | margin_right = 436.0 248 | margin_bottom = 178.0 249 | rect_min_size = Vector2( 0, 5 ) 250 | size_flags_vertical = 3 251 | 252 | [node name="Buttons" type="HBoxContainer" parent="Box"] 253 | margin_top = 182.0 254 | margin_right = 436.0 255 | margin_bottom = 214.0 256 | rect_min_size = Vector2( 0, 32 ) 257 | 258 | [node name="Cancel" type="Button" parent="Box/Buttons"] 259 | margin_right = 216.0 260 | margin_bottom = 32.0 261 | size_flags_horizontal = 3 262 | text = "Cancel" 263 | 264 | [node name="Accept" type="Button" parent="Box/Buttons"] 265 | margin_left = 220.0 266 | margin_right = 436.0 267 | margin_bottom = 32.0 268 | size_flags_horizontal = 3 269 | text = "Create" 270 | [connection signal="item_selected" from="Box/CompressInput/CompressMode" to="." method="_on_compression_selected"] 271 | [connection signal="pressed" from="Box/Buttons/Cancel" to="." method="_on_action" binds= [ false ]] 272 | [connection signal="pressed" from="Box/Buttons/Accept" to="." method="_on_action" binds= [ true ]] 273 | -------------------------------------------------------------------------------- /addons/better-texture-array/ui/edit_dialog.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorFileDialog 3 | 4 | const Layer = preload("res://addons/better-texture-array/ui/layer.gd") 5 | var channel_option: OptionButton 6 | var channel_option_box: HBoxContainer 7 | var selected_channel: int 8 | var layer 9 | 10 | func _init(): 11 | window_title = "Load image..." 12 | mode = EditorFileDialog.MODE_OPEN_FILE 13 | add_filter("*.png, *.jpg, *.webp, *.exr; Images") 14 | connect("file_selected", self, "_on_file_selected") 15 | 16 | var file_dialog_box = get_vbox() 17 | channel_option_box = HBoxContainer.new() 18 | channel_option_box.alignment = BoxContainer.ALIGN_END 19 | var channel_option_label = Label.new() 20 | channel_option_label.text = "Choose the channel" 21 | 22 | channel_option = OptionButton.new() 23 | channel_option.add_item("RED", Layer.Channels.RED) 24 | channel_option.add_item("GREEN", Layer.Channels.GREEN) 25 | channel_option.add_item("BLUE", Layer.Channels.BLUE) 26 | channel_option.add_item("ALPHA", Layer.Channels.ALPHA) 27 | 28 | channel_option_box.add_child(channel_option_label) 29 | channel_option_box.add_child(channel_option) 30 | file_dialog_box.add_child(channel_option_box) 31 | 32 | func _exit_tree(): 33 | disconnect("file_selected", self, "_on_file_selected") 34 | 35 | func popup_for_layer(lyr): 36 | layer = lyr 37 | # Only show the channel selection box if you have selected 38 | # a channel to view on the layer 39 | channel_option_box.visible = lyr.channel != Layer.Channels.ALL 40 | popup_centered_ratio() 41 | 42 | func _on_file_selected(path): 43 | var src = load(path) 44 | var img = src if src is Image else src.get_data() 45 | layer.update_layer(img, channel_option.get_selected_id()) 46 | -------------------------------------------------------------------------------- /addons/better-texture-array/ui/editor.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorProperty 3 | 4 | const Layer = preload("res://addons/better-texture-array/ui/layer.gd") 5 | const CreateDialog = preload("res://addons/better-texture-array/ui/create_dialog.tscn") 6 | const EditDialog = preload("res://addons/better-texture-array/ui/edit_dialog.gd") 7 | var undo_redo: UndoRedo 8 | var create_button 9 | var create_dialog 10 | var edit_dialog 11 | var layer_box 12 | var layer_list 13 | var layer_group 14 | var toolbar 15 | var tbb_vis 16 | var tbb_sep 17 | var tbb_grp 18 | var tbb_red 19 | var tbb_grn 20 | var tbb_blu 21 | var tbb_alp 22 | var tbb_all 23 | 24 | func _init(): 25 | label = "Layers" 26 | create_button = Button.new() 27 | create_button.text = "Create" 28 | create_dialog = CreateDialog.instance() 29 | edit_dialog = EditDialog.new() 30 | 31 | layer_box = VBoxContainer.new() 32 | layer_list = VBoxContainer.new() 33 | layer_group = ButtonGroup.new() 34 | 35 | toolbar = HBoxContainer.new() 36 | tbb_vis = CheckButton.new() 37 | tbb_sep = VSeparator.new() 38 | tbb_grp = ButtonGroup.new() 39 | tbb_red = Button.new() 40 | tbb_grn = Button.new() 41 | tbb_blu = Button.new() 42 | tbb_alp = Button.new() 43 | tbb_all = Button.new() 44 | 45 | toolbar.alignment = BoxContainer.ALIGN_CENTER 46 | tbb_sep.size_flags_horizontal = SIZE_EXPAND_FILL 47 | tbb_sep.add_stylebox_override("separator", StyleBoxEmpty.new()) 48 | tbb_vis.toggle_mode = true 49 | tbb_red.toggle_mode = true 50 | tbb_grn.toggle_mode = true 51 | tbb_blu.toggle_mode = true 52 | tbb_alp.toggle_mode = true 53 | tbb_all.toggle_mode = true 54 | tbb_vis.text = "Show" 55 | tbb_red.icon = preload("res://addons/better-texture-array/icons/RED.svg") 56 | tbb_grn.icon = preload("res://addons/better-texture-array/icons/GREEN.svg") 57 | tbb_blu.icon = preload("res://addons/better-texture-array/icons/BLUE.svg") 58 | tbb_alp.icon = preload("res://addons/better-texture-array/icons/ALPHA.svg") 59 | tbb_all.icon = preload("res://addons/better-texture-array/icons/ALL.svg") 60 | tbb_red.group = tbb_grp 61 | tbb_grn.group = tbb_grp 62 | tbb_blu.group = tbb_grp 63 | tbb_alp.group = tbb_grp 64 | tbb_all.group = tbb_grp 65 | tbb_all.pressed = true 66 | 67 | tbb_red.connect("toggled", self, "_toggle_channel", [Layer.Channels.RED]) 68 | tbb_grn.connect("toggled", self, "_toggle_channel", [Layer.Channels.GREEN]) 69 | tbb_blu.connect("toggled", self, "_toggle_channel", [Layer.Channels.BLUE]) 70 | tbb_alp.connect("toggled", self, "_toggle_channel", [Layer.Channels.ALPHA]) 71 | tbb_all.connect("toggled", self, "_toggle_channel", [Layer.Channels.ALL]) 72 | 73 | toolbar.add_child(tbb_vis) 74 | toolbar.add_child(tbb_sep) 75 | toolbar.add_child(tbb_red) 76 | toolbar.add_child(tbb_grn) 77 | toolbar.add_child(tbb_blu) 78 | toolbar.add_child(tbb_alp) 79 | toolbar.add_child(tbb_all) 80 | toolbar.add_constant_override("separation", 0) 81 | 82 | layer_list.visible = false 83 | layer_box.add_child(toolbar) 84 | layer_box.add_child(layer_list) 85 | 86 | tbb_vis.connect("toggled", self, "_toggle_layers") 87 | create_button.connect("pressed", self, "_open_create_dialog") 88 | create_dialog.connect("acknowledged", self, "_do_create_texarr") 89 | 90 | func update_list(): 91 | var texarr = get_edited_object() 92 | var children = layer_list.get_children() 93 | var have = children.size() 94 | var want = texarr.get_depth() if texarr else 0 95 | layer_list.rect_min_size.y = want * 192 96 | 97 | for i in have: 98 | var layer = children[i] 99 | layer_list.remove_child(layer) 100 | layer.disconnect("update_layer", self, "update_texarr") 101 | layer.queue_free() 102 | 103 | for i in want: 104 | var layer = Layer.new() 105 | layer.edit_dialog = edit_dialog 106 | layer_list.add_child(layer) 107 | layer.index = i 108 | layer.texture = texarr 109 | layer.rect_min_size = Vector2(128, 128) 110 | layer.group = layer_group 111 | layer.connect("update_layer", self, "update_texarr") 112 | 113 | func _ready(): 114 | label = "Layers" 115 | add_child(create_button) 116 | add_child(layer_box) 117 | add_child(create_dialog) 118 | add_child(edit_dialog) 119 | set_bottom_editor(layer_box) 120 | 121 | func update_property(): 122 | # Reset the visible state of the layers UI from the meta value `layers_visible` 123 | var texarr = get_edited_object() 124 | var vis = texarr.has_meta("layers_visible") and texarr.get_meta("layers_visible") 125 | layer_list.visible = vis 126 | tbb_vis.pressed = vis 127 | 128 | update_list() 129 | 130 | func create_texarr(width: int, height: int, depth: int, format: int, flags: int = Texture.FLAGS_DEFAULT): 131 | var texarr: TextureLayered = get_edited_object() 132 | var data = {"width": width, "height": height, "depth": depth, "format": format, "flags": flags, "layers": []} 133 | var img = Image.new() 134 | img.create(width, height, true, format) 135 | img.fill(Color.white) 136 | img.generate_mipmaps() 137 | for i in depth: 138 | data["layers"].append(img) 139 | emit_changed("data", data) 140 | # texarr.property_list_changed_notify() 141 | 142 | func update_texarr(src: Image, idx: int, src_chn: int, dst_chn: int): 143 | var texarr: TextureLayered = get_edited_object() 144 | var prv = texarr.get_layer_data(idx) 145 | undo_redo.create_action("Update layer") 146 | undo_redo.add_do_method(get_script(), "_update_layer", texarr, src, idx, src_chn, dst_chn) 147 | undo_redo.add_undo_method(get_script(), "_update_layer", texarr, prv, idx, src_chn, dst_chn) 148 | undo_redo.commit_action() 149 | 150 | static func _update_layer(texarr: TextureLayered, src, idx: int, chn_src: int = Layer.Channels.ALL, chn_dst: int = Layer.Channels.ALL): 151 | var dst: Image 152 | var size = Vector2(texarr.get_width(), texarr.get_height()) 153 | 154 | if src is Texture: 155 | src = src.get_data() 156 | if src.get_size() != size: 157 | src.resize(size.x, size.y) 158 | if src.is_compressed(): 159 | src.decompress() 160 | if src.get_format() != texarr.get_format(): 161 | src.convert(texarr.get_format()) 162 | 163 | if chn_src == Layer.Channels.ALL or chn_dst == Layer.Channels.ALL: 164 | dst = src 165 | else: 166 | dst = texarr.get_layer_data(idx) 167 | src.lock() 168 | dst.lock() 169 | for y in size.y: 170 | for x in size.x: 171 | var clr = dst.get_pixel(x, y) 172 | clr[chn_dst] = src.get_pixel(x, y)[chn_src] 173 | dst.set_pixel(x, y, clr) 174 | dst.unlock() 175 | src.unlock() 176 | 177 | dst.generate_mipmaps() 178 | texarr.set_layer_data(dst, idx) 179 | texarr.property_list_changed_notify() 180 | 181 | func _open_create_dialog(): 182 | create_dialog.popup_centered() 183 | 184 | func _do_create_texarr(ok, vals): 185 | create_texarr(vals[0], vals[1], vals[2], vals[3], vals[4]) 186 | 187 | func _toggle_layers(visible: bool): 188 | # Store the layers visible state in meta for use on UI reload in `update_property` 189 | get_edited_object().set_meta("layers_visible", visible) 190 | layer_list.visible = visible 191 | 192 | func _toggle_channel(visible: bool, chn: int): 193 | for layer in layer_list.get_children(): 194 | layer.channel = chn 195 | -------------------------------------------------------------------------------- /addons/better-texture-array/ui/layer.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Button 3 | 4 | enum Channels {RED, GREEN, BLUE, ALPHA, ALL} 5 | export(TextureLayered) var texture: TextureLayered setget set_texture 6 | export(int) var index = 0 setget set_index 7 | export(Channels) var channel = Channels.ALL setget set_channel 8 | 9 | var viewer 10 | var index_label 11 | var edit_dialog: EditorFileDialog 12 | 13 | signal update_layer 14 | 15 | func set_texture(v: TextureLayered): 16 | texture = v 17 | var is_3d = v is Texture3D 18 | viewer.material.set_shader_param("is_3d", is_3d) 19 | viewer.material.set_shader_param("tex3d" if is_3d else "texarr", v) 20 | 21 | func set_index(v: int): 22 | index = v 23 | index_label.text = str(v) 24 | viewer.material.set_shader_param("idx", v) 25 | 26 | func set_channel(v: int): 27 | channel = v 28 | var chn = Color(1, 1, 1, 1) 29 | if v < Channels.ALL: 30 | chn = Color(0, 0, 0, 0) 31 | chn[v] = 1 32 | viewer.material.set_shader_param("chn", chn) 33 | 34 | func _init(): 35 | toggle_mode = true 36 | size_flags_vertical = SIZE_EXPAND_FILL 37 | viewer = ColorRect.new() 38 | viewer.mouse_filter = MOUSE_FILTER_IGNORE 39 | viewer.material = ShaderMaterial.new() 40 | viewer.material.shader = preload("res://addons/better-texture-array/ui/layer.shader") 41 | viewer.rect_min_size = Vector2(64, 64) 42 | index_label = Label.new() 43 | index_label.rect_position = Vector2(4, 4) 44 | 45 | add_child(viewer) 46 | add_child(index_label) 47 | 48 | func _notification(what): 49 | if what == NOTIFICATION_RESIZED: 50 | var pad = 4 51 | var dim = rect_size.y - pad 52 | var off = Vector2((rect_size.x - dim) / 2.0, 2) 53 | viewer.rect_size = Vector2(dim, dim) 54 | viewer.rect_position = off 55 | 56 | func _toggled(pressed: bool): 57 | if pressed: 58 | texture.set_meta("layer_selected", index) 59 | texture.emit_signal("changed") 60 | 61 | func _input(event): 62 | if is_hovered() and event is InputEventMouseButton and event.doubleclick: 63 | edit_dialog.popup_for_layer(self) 64 | 65 | func update_layer(img, img_chn): 66 | emit_signal("update_layer", img, index, img_chn, channel) 67 | -------------------------------------------------------------------------------- /addons/better-texture-array/ui/layer.shader: -------------------------------------------------------------------------------- 1 | shader_type canvas_item; 2 | 3 | uniform bool is_3d = false; 4 | uniform sampler3D tex3d : hint_black; 5 | uniform sampler2DArray texarr : hint_black; 6 | uniform float idx = 0; 7 | uniform vec4 chn = vec4(1); 8 | 9 | void fragment() { 10 | vec4 lyr; 11 | if (is_3d) { 12 | lyr = texture(tex3d, vec3(UV, idx)); 13 | } else { 14 | lyr = texture(texarr, vec3(UV, idx)); 15 | } 16 | 17 | // COLOR = all(bvec4(chn)) ? lyr : vec4(dot(lyr, chn)); 18 | bvec4 bchn = bvec4(chn); 19 | lyr = lyr * chn; 20 | COLOR = all(bchn) ? lyr : (all(not(bchn.rgb)) && bchn.a ? vec4(lyr.a) : vec4(lyr.rgb, 1)); 21 | } -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkwardpolygons/better-texture-array/3fc4b572f4ce93046469535e6fddc7c14fd4d57a/icon.png -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=4 10 | 11 | _global_script_classes=[ { 12 | "base": "ResourceFormatSaver", 13 | "class": "TextureArraySaver", 14 | "language": "GDScript", 15 | "path": "res://addons/better-texture-array/texture_array_saver.gd" 16 | } ] 17 | _global_script_class_icons={ 18 | "TextureArraySaver": "" 19 | } 20 | 21 | [application] 22 | 23 | config/name="BetterTextureArray" 24 | config/icon="res://icon.png" 25 | 26 | [editor_plugins] 27 | 28 | enabled=PoolStringArray( "better-texture-array" ) 29 | 30 | [rendering] 31 | 32 | environment/default_environment="res://default_env.tres" 33 | -------------------------------------------------------------------------------- /screenshots/.gdignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkwardpolygons/better-texture-array/3fc4b572f4ce93046469535e6fddc7c14fd4d57a/screenshots/.gdignore -------------------------------------------------------------------------------- /screenshots/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkwardpolygons/better-texture-array/3fc4b572f4ce93046469535e6fddc7c14fd4d57a/screenshots/screenshot.png --------------------------------------------------------------------------------