├── .gitattributes ├── .gitignore ├── LICENSE.txt ├── README.md ├── addons └── fusion_compute │ ├── LICENSE.txt │ ├── compute.gd │ ├── compute.gd.uid │ ├── examples │ ├── array_multiply │ │ ├── array_multiply.gd │ │ ├── array_multiply.gd.uid │ │ ├── array_multiply.glsl │ │ ├── array_multiply.glsl.import │ │ └── array_multiply.tscn │ ├── generate_image │ │ ├── generate_image.gd │ │ ├── generate_image.gd.uid │ │ ├── generate_image.glsl │ │ ├── generate_image.glsl.import │ │ └── generate_image.tscn │ └── slime │ │ ├── agents.glsl │ │ ├── agents.glsl.import │ │ ├── diffuse.glsl │ │ ├── diffuse.glsl.import │ │ ├── display.gdshader │ │ ├── display.gdshader.uid │ │ ├── slime.gd │ │ ├── slime.gd.uid │ │ └── slime.tscn │ └── plugin.cfg ├── build.py ├── icon.png ├── icon.png.import └── project.godot /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4 2 | .godot/ 3 | 4 | # Build result from python script 5 | fusion-compute-plugin.zip 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 SmallConfusion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fusion Compute 2 | 3 | This is an plugin for Godot designed to make using compute shader less painful. Supports Godot 4.3 - 4.4rc2, most likely works on all 4.x versions. 4 | 5 | When using compute shaders normally, you have to write many lines of boilerplate just to make a basic "hello world" program. I wrote this plugin to fix that. 6 | 7 | For example, [the godot docs tutorial for compute shaders](https://docs.godotengine.org/en/stable/tutorials/shaders/compute_shaders.html), which multiplies an array by two, uses around ~20 lines of gdscript. This plugin reduces it to much less: 8 | 9 | ```gdscript 10 | func _ready() -> void: 11 | var compute := Compute.new("res://shader.glsl", 1, 1, 1) 12 | 13 | var data := PackedFloat32Array(range(16)) 14 | compute.create_data(data.to_byte_array()) 15 | 16 | compute.submit() 17 | compute.sync() 18 | 19 | var result := compute.get_data(0).to_float32_array()) 20 | ``` 21 | 22 | More examples can be found in the [examples folder.](./addons/fusion_compute/examples/) 23 | 24 | There are only a few ways to interact with compute shaders. This plugin supports data buffers, images, and push constants. In addition, this plugin also supports multiple pipelines using the same buffers. Anything more than that (eg. [recreating this example project with swapping RIDs through uniform sets](https://github.com/godotengine/godot-demo-projects/tree/master/compute/texture/water_plane)) is not supported. I potentially want to add support for more things like that in the future, but the first priortiy of this plugin is to make interacting with compute shaders as simple as possible, rather than abstracting everything you could want to do with a compute shader. 25 | 26 | Shaders themselves are not changed by this plugin, they are written exactly the same, only the gdscript boilerplate is abstracted away. 27 | 28 | ## Documentation 29 | 30 | Documentation of functions and usage can be found by reading the generated documentation of the Compute class in the Godot engine. Reading the [multiply_arrays](./addons/fusion_compute/examples/array_multiply/) and [generate_image](./addons/fusion_compute/examples/generate_image/) will be helpful too. The [slime](./addons/fusion_compute/examples/slime/) example is more complex. 31 | -------------------------------------------------------------------------------- /addons/fusion_compute/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 SmallConfusion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/fusion_compute/compute.gd: -------------------------------------------------------------------------------- 1 | class_name Compute 2 | ## Compute shader helper. 3 | ## 4 | ## When creating data buffers or images, they should be created in binding 5 | ## order, as the first one created will be binding = 0, 6 | ## the second one will be binding = 1, and so on. 7 | 8 | var _rd: RenderingDevice 9 | 10 | var _uniforms: Array[RDUniform] = [] 11 | var _buffers: Array[_Buffer] = [] 12 | var _pipelines: Array[_Pipeline] = [] 13 | 14 | var _uses_global_rd: bool 15 | var _lock := false 16 | 17 | ## Creates an instance of [Compute].[br] 18 | ## 19 | ## [param wg_count_x], [param y], and [param z] are the number of groups that 20 | ## this compute shader is dispatched on.[br] 21 | ## 22 | ## [param use_global_rd] uses the global rendering device rather than creating a 23 | ## local one. I don't know the consequences of this but it allows you to use a 24 | ## [Texture2DRD]. It causes a bunch of error messages in 4.4 in the slime 25 | ## example, I'm not sure how to properly use it. 26 | func _init( 27 | shader_path: String, 28 | wg_count_x := 1, 29 | wg_count_y := 1, 30 | wg_count_z := 1, 31 | use_global_rd := false, 32 | ) -> void: 33 | _uses_global_rd = use_global_rd 34 | 35 | if use_global_rd: 36 | _rd = RenderingServer.get_rendering_device() 37 | else: 38 | _rd = RenderingServer.create_local_rendering_device() 39 | 40 | create_pipeline(shader_path, wg_count_x, wg_count_y, wg_count_z) 41 | 42 | ## Creates a new [Compute] object. 43 | ## @deprecated: Use `Compute.new()` instead. 44 | static func create( 45 | shader_path: String, 46 | wg_count_x := 1, 47 | wg_count_y := 1, 48 | wg_count_z := 1, 49 | use_global_rd := false 50 | ) -> Compute: 51 | 52 | return Compute.new( 53 | shader_path, 54 | wg_count_x, 55 | wg_count_y, 56 | wg_count_z, 57 | use_global_rd 58 | ) 59 | 60 | ## Changes the work group count on a pipeline. The default pipeline is 0. 61 | func update_wg_count( 62 | wg_count_x := 1, 63 | wg_count_y := 1, 64 | wg_count_z := 1, 65 | pipeline := 0 66 | ) -> void: 67 | 68 | _pipelines[pipeline].wgx = wg_count_x 69 | _pipelines[pipeline].wgy = wg_count_y 70 | _pipelines[pipeline].wgz = wg_count_z 71 | 72 | ## Creates a data buffer from the provided [PackedByteArray]. It returns the 73 | ## binding that this buffer is on. 74 | func create_data(data: PackedByteArray) -> int: 75 | assert(!_lock, "Attempted to create new data buffer after running.") 76 | 77 | var binding := len(_uniforms) 78 | 79 | var uniform = RDUniform.new() 80 | uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER 81 | uniform.binding = binding 82 | 83 | var buffer := _Buffer.new() 84 | buffer.type = _Buffer.Usage.DATA 85 | 86 | buffer.rid = _rd.storage_buffer_create(data.size(), data) 87 | uniform.add_id(buffer.rid) 88 | 89 | _buffers.append(buffer) 90 | _uniforms.append(uniform) 91 | 92 | return binding 93 | 94 | ## Updates data on the provided buffer. 95 | func update_data( 96 | binding: int, 97 | data: PackedByteArray, 98 | offset := 0, 99 | size := -1 100 | ) -> void: 101 | 102 | _validate_binding(binding, _Buffer.Usage.DATA) 103 | 104 | if size == -1: 105 | size = data.size() 106 | 107 | _rd.buffer_update(_buffers[binding].rid, 0, size, data) 108 | 109 | ## Gets the data from the provided buffer. 110 | func get_data(binding: int, offset := 0, size := 0) -> PackedByteArray: 111 | _validate_binding(binding, _Buffer.Usage.DATA) 112 | return _rd.buffer_get_data(_buffers[binding].rid, offset, size) 113 | 114 | ## Initializes an image.[br] 115 | ## 116 | ## This function does not assign anything to this image, 117 | ## [member update_image()] should be called after this to provide image data. Returns the 118 | ## binding that this image was created on. 119 | func create_image( 120 | width: int, 121 | height: int, 122 | format: RenderingDevice.DataFormat, 123 | usage_bits: int 124 | ) -> int: 125 | 126 | var image_format := RDTextureFormat.new() 127 | image_format.width = width 128 | image_format.height = height 129 | image_format.format = format 130 | image_format.usage_bits = usage_bits 131 | 132 | return create_image_from_format(image_format) 133 | 134 | ## Initializes an image with the provided format.[br] 135 | ## 136 | ## This function does not assign 137 | ## anything to this image, [member update_image()] should be called after this 138 | ## to provide image data. Returns the binding that this image was created on. 139 | func create_image_from_format(format: RDTextureFormat) -> int: 140 | assert(!_lock, "Attempted to create new image buffer after running.") 141 | 142 | var binding = len(_uniforms) 143 | 144 | var buffer := _Buffer.new() 145 | 146 | buffer.type = _Buffer.Usage.IMAGE 147 | 148 | buffer.rid = _rd.texture_create(format, RDTextureView.new()) 149 | 150 | var uniform := RDUniform.new() 151 | uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE 152 | uniform.binding = binding 153 | uniform.add_id(buffer.rid) 154 | 155 | _buffers.append(buffer) 156 | _uniforms.append(uniform) 157 | 158 | return binding 159 | 160 | ## Updates image data on the provided binding. 161 | func update_image(binding: int, data: PackedByteArray) -> void: 162 | _validate_binding(binding, _Buffer.Usage.IMAGE) 163 | _rd.texture_update(_buffers[binding].rid, 0, data) 164 | 165 | ## Gets the image data from the provided binding. 166 | func get_image(binding: int) -> PackedByteArray: 167 | _validate_binding(binding, _Buffer.Usage.IMAGE) 168 | return _rd.texture_get_data(_buffers[binding].rid, 0) 169 | 170 | ## Clears the image data on the provided binding. 171 | func clear_image(binding: int, color: Color) -> void: 172 | _validate_binding(binding, _Buffer.Usage.IMAGE) 173 | _rd.texture_clear(_buffers[binding].rid, color, 0, 1, 0, 1) 174 | 175 | ## Returns the rid of the image on the provided binding, for use with a 176 | ## [Texture2DRD]. Make sure that this compute object was created with 177 | ## [code]use_global_rd = true[/code], otherwise this will not work, though doing 178 | ## that seems to cause errors in 4.4. 179 | func get_image_rid(binding: int) -> RID: 180 | _validate_binding(binding, _Buffer.Usage.IMAGE) 181 | 182 | assert( 183 | _uses_global_rd, 184 | "Compute needs to be created with use_global_rd = true to use get_image_rid()" 185 | ) 186 | 187 | return _buffers[binding].rid 188 | 189 | ## Submits the compute shader on a given pipeline, the default pipeline is 0. 190 | ## If a [PackedByteArray] of your push constants is provided, they will be 191 | ## passed to the shader. 192 | func submit(push_constant := PackedByteArray(), pipeline := 0) -> void: 193 | _lock = true 194 | 195 | var p := _pipelines[pipeline] 196 | 197 | if not p.uniform_set.is_valid(): 198 | p.uniform_set = _rd.uniform_set_create(_uniforms, p.shader, 0) 199 | 200 | var compute_list = _rd.compute_list_begin() 201 | _rd.compute_list_bind_compute_pipeline(compute_list, p.pipeline) 202 | _rd.compute_list_bind_uniform_set(compute_list, p.uniform_set, 0) 203 | 204 | if len(push_constant) != 0: 205 | while push_constant.size() % 16 != 0: 206 | push_constant.append(0) 207 | 208 | _rd.compute_list_set_push_constant( 209 | compute_list, 210 | push_constant, 211 | push_constant.size() 212 | ) 213 | 214 | _rd.compute_list_dispatch(compute_list, p.wgx, p.wgy, p.wgz) 215 | _rd.compute_list_end() 216 | 217 | _rd.submit() 218 | 219 | ## Syncs the shader. 220 | func sync() -> void: 221 | _rd.sync() 222 | 223 | ## Performs cleanup, freeing data from the gpu. This should be called when 224 | ## you're finished with the [Compute] object, to avoid memory leaks. 225 | func cleanup() -> void: 226 | for p in _pipelines: 227 | p.cleanup(_rd) 228 | 229 | for buffer in _buffers: 230 | _rd.free_rid(buffer) 231 | 232 | _rd.free() 233 | 234 | ## Creates another pipeline on this [Compute] object. Buffers are shared between 235 | ## all pipelines. 236 | func create_pipeline(shader_path: String, wg_count_x := 1, wg_count_y := 1, wg_count_z := 1) -> int: 237 | var p := _Pipeline.new() 238 | 239 | p.wgx = wg_count_x 240 | p.wgy = wg_count_y 241 | p.wgz = wg_count_z 242 | 243 | var f := load(shader_path) 244 | var spirv: RDShaderSPIRV = f.get_spirv() 245 | p.shader = _rd.shader_create_from_spirv(spirv) 246 | 247 | p.pipeline = _rd.compute_pipeline_create(p.shader) 248 | 249 | _pipelines.append(p) 250 | 251 | return len(_pipelines) - 1 252 | 253 | func _validate_binding(binding: int, type: _Buffer.Usage): 254 | assert(len(_buffers) > binding, "Binding %d does not exist!" % binding) 255 | 256 | assert( 257 | _buffers[binding].type == type, 258 | "Buffer %d is of type %s, not %s" % [ 259 | binding, 260 | _Buffer.usage_string(_buffers[binding].type), 261 | _Buffer.usage_string(type) 262 | ] 263 | ) 264 | 265 | class _Pipeline: 266 | var shader: RID 267 | var pipeline: RID 268 | var uniform_set: RID 269 | 270 | var wgx: int 271 | var wgy: int 272 | var wgz: int 273 | 274 | func cleanup(rd: RenderingDevice): 275 | rd.free_rid(shader) 276 | rd.free_rid(pipeline) 277 | rd.free_rid(uniform_set) 278 | 279 | class _Buffer: 280 | enum Usage {DATA, IMAGE} 281 | 282 | var rid: RID 283 | var type: Usage 284 | 285 | static func usage_string(usage: Usage) -> String: 286 | match usage: 287 | Usage.DATA: 288 | return "data" 289 | Usage.IMAGE: 290 | return "image" 291 | _: 292 | return "unknown" 293 | -------------------------------------------------------------------------------- /addons/fusion_compute/compute.gd.uid: -------------------------------------------------------------------------------- 1 | uid://deqs5fx8tvrou 2 | -------------------------------------------------------------------------------- /addons/fusion_compute/examples/array_multiply/array_multiply.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | 4 | func _ready() -> void: 5 | var c := Compute.new( 6 | "res://addons/fusion_compute/examples/array_multiply/array_multiply.glsl", 7 | 1, 1, 1 8 | ) 9 | 10 | var data := PackedFloat32Array(range(64)) 11 | 12 | print("Data before: ", data) 13 | c.create_data(data.to_byte_array()) 14 | 15 | print("Submitting") 16 | c.submit(PackedFloat32Array([4]).to_byte_array()) 17 | c.sync() 18 | print("Synced") 19 | 20 | data = c.get_data(0).to_float32_array() 21 | print("Data after: ", data) 22 | -------------------------------------------------------------------------------- /addons/fusion_compute/examples/array_multiply/array_multiply.gd.uid: -------------------------------------------------------------------------------- 1 | uid://pytd06uy5wub 2 | -------------------------------------------------------------------------------- /addons/fusion_compute/examples/array_multiply/array_multiply.glsl: -------------------------------------------------------------------------------- 1 | #[compute] 2 | #version 450 3 | 4 | layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; 5 | 6 | layout(set = 0, binding = 0, std430) restrict buffer Array { 7 | float data[]; 8 | } 9 | array; 10 | 11 | layout(push_constant) uniform PushConstants { 12 | float multiplier; 13 | } 14 | pc; 15 | 16 | 17 | void main() { 18 | array.data[gl_GlobalInvocationID.x] *= pc.multiplier; 19 | } -------------------------------------------------------------------------------- /addons/fusion_compute/examples/array_multiply/array_multiply.glsl.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="glsl" 4 | type="RDShaderFile" 5 | uid="uid://cee161gc6f37m" 6 | path="res://.godot/imported/array_multiply.glsl-51f1b9d2149e21aec451099a8edc14eb.res" 7 | 8 | [deps] 9 | 10 | source_file="res://addons/fusion_compute/examples/array_multiply/array_multiply.glsl" 11 | dest_files=["res://.godot/imported/array_multiply.glsl-51f1b9d2149e21aec451099a8edc14eb.res"] 12 | 13 | [params] 14 | 15 | -------------------------------------------------------------------------------- /addons/fusion_compute/examples/array_multiply/array_multiply.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://ddawqlag45ypk"] 2 | 3 | [ext_resource type="Script" uid="uid://pytd06uy5wub" path="res://addons/fusion_compute/examples/array_multiply/array_multiply.gd" id="1_u2my6"] 4 | 5 | [node name="Node2D" type="Node2D"] 6 | script = ExtResource("1_u2my6") 7 | -------------------------------------------------------------------------------- /addons/fusion_compute/examples/generate_image/generate_image.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | const image_size = 512 4 | 5 | func _ready() -> void: 6 | @warning_ignore("integer_division") 7 | var c := Compute.new( 8 | "res://addons/fusion_compute/examples/generate_image/generate_image.glsl", 9 | image_size / 8, image_size / 8, 1 10 | ) 11 | 12 | c.create_image( 13 | image_size, 14 | image_size, 15 | RenderingDevice.DATA_FORMAT_R8G8B8A8_UNORM, 16 | 17 | RenderingDevice.TEXTURE_USAGE_STORAGE_BIT + \ 18 | RenderingDevice.TEXTURE_USAGE_CAN_COPY_FROM_BIT + \ 19 | RenderingDevice.TEXTURE_USAGE_CAN_UPDATE_BIT 20 | ) 21 | 22 | c.submit() 23 | c.sync() 24 | 25 | var image_data := c.get_image(0) 26 | var image := Image.create_from_data( 27 | 512, 512, false, Image.FORMAT_RGBA8, image_data 28 | ) 29 | 30 | $TextureRect.texture = ImageTexture.create_from_image(image) 31 | -------------------------------------------------------------------------------- /addons/fusion_compute/examples/generate_image/generate_image.gd.uid: -------------------------------------------------------------------------------- 1 | uid://blxbwsr1qyiqq 2 | -------------------------------------------------------------------------------- /addons/fusion_compute/examples/generate_image/generate_image.glsl: -------------------------------------------------------------------------------- 1 | #[compute] 2 | #version 450 3 | 4 | layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; 5 | 6 | layout(rgba8, binding = 0) restrict uniform image2D image; 7 | 8 | 9 | void main() { 10 | ivec2 coords = ivec2(gl_GlobalInvocationID.xy); 11 | ivec2 dim = imageSize(image); 12 | 13 | if (coords.x > dim.x || coords.y > dim.y) { 14 | return; 15 | } 16 | 17 | vec2 uv = vec2(coords) / vec2(dim); 18 | 19 | vec4 c = vec4(uv.x, uv.y, 1.0, 1.0); 20 | 21 | imageStore(image, coords, c); 22 | } -------------------------------------------------------------------------------- /addons/fusion_compute/examples/generate_image/generate_image.glsl.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="glsl" 4 | type="RDShaderFile" 5 | uid="uid://dsoh0wx2fc28" 6 | path="res://.godot/imported/generate_image.glsl-2793b4d3a41537d0b0205ba0c3bd683d.res" 7 | 8 | [deps] 9 | 10 | source_file="res://addons/fusion_compute/examples/generate_image/generate_image.glsl" 11 | dest_files=["res://.godot/imported/generate_image.glsl-2793b4d3a41537d0b0205ba0c3bd683d.res"] 12 | 13 | [params] 14 | 15 | -------------------------------------------------------------------------------- /addons/fusion_compute/examples/generate_image/generate_image.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://d3k3igu00npky"] 2 | 3 | [ext_resource type="Script" uid="uid://blxbwsr1qyiqq" path="res://addons/fusion_compute/examples/generate_image/generate_image.gd" id="1_6enyk"] 4 | 5 | [node name="GenerateImage" type="Control"] 6 | layout_mode = 3 7 | anchors_preset = 15 8 | anchor_right = 1.0 9 | anchor_bottom = 1.0 10 | grow_horizontal = 2 11 | grow_vertical = 2 12 | script = ExtResource("1_6enyk") 13 | 14 | [node name="TextureRect" type="TextureRect" parent="."] 15 | custom_minimum_size = Vector2(512, 512) 16 | layout_mode = 1 17 | anchors_preset = 8 18 | anchor_left = 0.5 19 | anchor_top = 0.5 20 | anchor_right = 0.5 21 | anchor_bottom = 0.5 22 | offset_left = -256.0 23 | offset_top = -256.0 24 | offset_right = 256.0 25 | offset_bottom = 256.0 26 | grow_horizontal = 2 27 | grow_vertical = 2 28 | expand_mode = 1 29 | -------------------------------------------------------------------------------- /addons/fusion_compute/examples/slime/agents.glsl: -------------------------------------------------------------------------------- 1 | #[compute] 2 | #version 450 3 | 4 | layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; 5 | 6 | layout(set = 0, binding = 0, std430) restrict buffer Agents { 7 | float data[]; 8 | } 9 | agents; 10 | 11 | layout(r32f, binding = 1) restrict uniform image2D trailmap; 12 | 13 | layout(push_constant) uniform PushConstants { 14 | float trailStrength; 15 | float width; 16 | float height; 17 | float sensor_angle; 18 | float sensor_distance; 19 | float speed; 20 | float turning; 21 | float random; 22 | float time; 23 | } 24 | pc; 25 | 26 | 27 | const float TAU = 6.28318530718; 28 | 29 | 30 | vec2 rotate(vec2 v, float a) { 31 | return vec2( 32 | v.x * cos(a) - v.y * sin(a), 33 | v.x * sin(a) + v.y * cos(a) 34 | ); 35 | } 36 | 37 | vec2 angle_vec(float a) { 38 | return vec2(cos(a), sin(a)); 39 | } 40 | 41 | float random(vec2 st) { 42 | st = mod(st, vec2(1000)); 43 | return fract(sin(dot(st.xy, vec2(12.9898,78.233)))*43758.5453123); 44 | } 45 | 46 | vec2 loop_screen(vec2 pos) { 47 | return mod(pos, vec2(pc.width, pc.height)); 48 | } 49 | 50 | 51 | void main() { 52 | // Setup 53 | vec2 pos = vec2( 54 | agents.data[gl_GlobalInvocationID.x * 3], 55 | agents.data[gl_GlobalInvocationID.x * 3 + 1] 56 | ); 57 | 58 | float angle = agents.data[gl_GlobalInvocationID.x * 3 + 2]; 59 | 60 | pos = loop_screen(pos); 61 | 62 | // Sensors 63 | vec2 forward = angle_vec(angle) * pc.sensor_distance; 64 | vec2 left = angle_vec(angle - pc.sensor_angle) * pc.sensor_distance; 65 | vec2 right = angle_vec(angle + pc.sensor_angle) * pc.sensor_distance; 66 | 67 | float f_val = imageLoad(trailmap, ivec2(loop_screen(pos + forward))).r; 68 | float l_val = imageLoad(trailmap, ivec2(loop_screen(pos + left))).r; 69 | float r_val = imageLoad(trailmap, ivec2(loop_screen(pos + right))).r; 70 | 71 | // Steering 72 | float random_steer = 2.0 * (random(pc.time * pos * forward) - 0.5); 73 | 74 | if (f_val > max(l_val, r_val)) { 75 | // angle += 0; 76 | } else if (f_val < min(l_val, r_val)) { 77 | angle += pc.turning * random_steer; 78 | } else if (r_val > l_val) { 79 | angle += pc.turning + random_steer * pc.random; 80 | } else { 81 | angle -= pc.turning + random_steer * pc.random; 82 | } 83 | 84 | vec2 vel = angle_vec(angle) * pc.speed; 85 | 86 | // Should use atomic add here I think, but idk how to do that. 87 | float under = imageLoad(trailmap, ivec2(pos)).r; 88 | imageStore(trailmap, ivec2(pos), vec4(pc.trailStrength + under)); 89 | 90 | pos += vel; 91 | 92 | // Store 93 | agents.data[gl_GlobalInvocationID.x * 3] = pos.x; 94 | agents.data[gl_GlobalInvocationID.x * 3 + 1] = pos.y; 95 | agents.data[gl_GlobalInvocationID.x * 3 + 2] = angle; 96 | } -------------------------------------------------------------------------------- /addons/fusion_compute/examples/slime/agents.glsl.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="glsl" 4 | type="RDShaderFile" 5 | uid="uid://c441mxxby5ybe" 6 | path="res://.godot/imported/agents.glsl-d0f88125640614e95b0e826a09bb6a21.res" 7 | 8 | [deps] 9 | 10 | source_file="res://addons/fusion_compute/examples/slime/agents.glsl" 11 | dest_files=["res://.godot/imported/agents.glsl-d0f88125640614e95b0e826a09bb6a21.res"] 12 | 13 | [params] 14 | 15 | -------------------------------------------------------------------------------- /addons/fusion_compute/examples/slime/diffuse.glsl: -------------------------------------------------------------------------------- 1 | #[compute] 2 | #version 450 3 | 4 | layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; 5 | 6 | layout(r32f, binding = 1) restrict readonly uniform image2D trailmap_i; 7 | layout(r32f, binding = 2) restrict writeonly uniform image2D trailmap_o; 8 | 9 | layout(push_constant) uniform PushConstants { 10 | float evaporation; 11 | float diffusion; 12 | } 13 | pc; 14 | 15 | void main() { 16 | ivec2 coords = ivec2(gl_GlobalInvocationID.xy); 17 | ivec2 dims = imageSize(trailmap_i); 18 | 19 | if (coords.x > dims.x || coords.y > dims.y) { 20 | return; 21 | } 22 | 23 | float v = imageLoad(trailmap_i, coords).r; 24 | 25 | // Conservation of matter, diffusion cannot create more. 26 | v -= v * pc.diffusion * 4.0; 27 | v *= pc.evaporation; 28 | 29 | v += imageLoad(trailmap_i, ivec2(mod(coords + ivec2(1, 0), dims))).r * pc.diffusion; 30 | v += imageLoad(trailmap_i, ivec2(mod(coords + ivec2(-1, 0), dims))).r * pc.diffusion; 31 | v += imageLoad(trailmap_i, ivec2(mod(coords + ivec2(0, 1), dims))).r * pc.diffusion; 32 | v += imageLoad(trailmap_i, ivec2(mod(coords + ivec2(0, -1), dims))).r * pc.diffusion; 33 | 34 | v = max(v, 0); 35 | 36 | imageStore(trailmap_o, coords, vec4(v)); 37 | } 38 | -------------------------------------------------------------------------------- /addons/fusion_compute/examples/slime/diffuse.glsl.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="glsl" 4 | type="RDShaderFile" 5 | uid="uid://d3umgr625tig8" 6 | path="res://.godot/imported/diffuse.glsl-c0b038012adcc971e2157cb08416f8cd.res" 7 | 8 | [deps] 9 | 10 | source_file="res://addons/fusion_compute/examples/slime/diffuse.glsl" 11 | dest_files=["res://.godot/imported/diffuse.glsl-c0b038012adcc971e2157cb08416f8cd.res"] 12 | 13 | [params] 14 | 15 | -------------------------------------------------------------------------------- /addons/fusion_compute/examples/slime/display.gdshader: -------------------------------------------------------------------------------- 1 | shader_type canvas_item; 2 | 3 | void fragment() { 4 | float r = texture(TEXTURE, UV).r; 5 | 6 | r = r / (r + 1.0); 7 | 8 | COLOR.rgb = r * vec3(1); 9 | } -------------------------------------------------------------------------------- /addons/fusion_compute/examples/slime/display.gdshader.uid: -------------------------------------------------------------------------------- 1 | uid://dsww2pya7puex 2 | -------------------------------------------------------------------------------- /addons/fusion_compute/examples/slime/slime.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # This causes a whole bunch of errors in 4.4 because we are using the global 4 | # RenderingDevice. I don't know what changed but it still seems to work and I 5 | # can't be bothered to fix it right now. 6 | 7 | const width := 1024 8 | const height := 1024 9 | 10 | const wg_size := 64 11 | 12 | @warning_ignore("integer_division") 13 | const wg_count := 1000000 / wg_size 14 | 15 | const agent_count := wg_count * wg_size 16 | 17 | 18 | @export_range(0, 1, 0.00001, "or_less") var evaporation := 0.99 19 | @export_range(0, 0.25, 0.0001) var diffusion := 0.1 20 | 21 | @export_range(0, 1, 0.0001, "or_greater", "or_less") var trail_strength := 0.1 22 | @export_range(0, PI, 0.0001, "or_greater") var sensor_angle := PI / 180.0 * 30.0; 23 | @export_range(0, 40, 0.01, "or_greater") var sensor_distance := 1.5; 24 | 25 | @export_range(0, 5, 0.0001, "or_greater", "or_less") var speed := 1.0; 26 | @export_range(0, 5, 0.00001, "or_greater") var turning := 0.1; 27 | @export_range(0, 0.2, 0.00001, "or_greater") var random := 0.005; 28 | 29 | 30 | var compute: Compute 31 | 32 | 33 | func _ready() -> void: 34 | compute = Compute.new( 35 | "res://addons/fusion_compute/examples/slime/agents.glsl", 36 | wg_count, 1, 1, true 37 | ) 38 | 39 | @warning_ignore("integer_division") 40 | compute.create_pipeline( 41 | "res://addons/fusion_compute/examples/slime/diffuse.glsl", 42 | width / 8, height / 8 43 | ) 44 | 45 | var agent_data := _agents_circle().to_byte_array() 46 | 47 | compute.create_data(agent_data) 48 | 49 | compute.create_image( 50 | width, 51 | height, 52 | RenderingDevice.DATA_FORMAT_R32_SFLOAT, 53 | 54 | RenderingDevice.TEXTURE_USAGE_STORAGE_BIT + \ 55 | RenderingDevice.TEXTURE_USAGE_CAN_UPDATE_BIT + \ 56 | RenderingDevice.TEXTURE_USAGE_CAN_COPY_FROM_BIT 57 | ) 58 | 59 | compute.create_image( 60 | width, 61 | height, 62 | RenderingDevice.DATA_FORMAT_R32_SFLOAT, 63 | 64 | RenderingDevice.TEXTURE_USAGE_STORAGE_BIT + \ 65 | RenderingDevice.TEXTURE_USAGE_CAN_COPY_FROM_BIT + \ 66 | RenderingDevice.TEXTURE_USAGE_SAMPLING_BIT 67 | ) 68 | 69 | for child in get_children(): 70 | if child is TextureRect: 71 | var tex = child.texture 72 | 73 | if tex is Texture2DRD: 74 | tex.texture_rd_rid = compute.get_image_rid(2) 75 | 76 | 77 | func _process(_delta: float) -> void: 78 | compute.submit( 79 | PackedFloat32Array([ 80 | trail_strength, 81 | float(width), 82 | float(height), 83 | sensor_angle, 84 | sensor_distance, 85 | speed, 86 | turning, 87 | random, 88 | float(Time.get_ticks_msec()) / 1000.0 89 | ]).to_byte_array(), 90 | 0 91 | ) 92 | 93 | compute. sync () 94 | 95 | compute.submit( 96 | PackedFloat32Array([evaporation, diffusion]).to_byte_array(), 1 97 | ) 98 | 99 | compute. sync () 100 | 101 | var image_data := compute.get_image(2) 102 | compute.update_image(1, image_data) 103 | 104 | 105 | func _agents_random() -> PackedFloat32Array: 106 | var a := PackedFloat32Array() 107 | a.resize(agent_count * 3) 108 | 109 | for i in range(agent_count): 110 | a[i * 3] = randf_range(0, width) 111 | a[i * 3 + 1] = randf_range(0, height) 112 | a[i * 3 + 2] = randf_range(0, TAU) 113 | 114 | return a 115 | 116 | 117 | func _agents_circle() -> PackedFloat32Array: 118 | var a := PackedFloat32Array() 119 | a.resize(agent_count * 3) 120 | 121 | var radius := width / 2.0 - 100 122 | var center := Vector2(width, height) / 2 123 | 124 | for i in range(agent_count): 125 | var angle := Vector2.from_angle(randf() * TAU) 126 | var pos := angle * radius * sqrt(randf()) + center 127 | 128 | a[i * 3] = pos.x; 129 | a[i * 3 + 1] = pos.y; 130 | a[i * 3 + 2] = pos.angle_to_point(center) 131 | 132 | return a 133 | -------------------------------------------------------------------------------- /addons/fusion_compute/examples/slime/slime.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cjvpdik1cjiaq 2 | -------------------------------------------------------------------------------- /addons/fusion_compute/examples/slime/slime.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://duxqc645il4la"] 2 | 3 | [ext_resource type="Script" uid="uid://cjvpdik1cjiaq" path="res://addons/fusion_compute/examples/slime/slime.gd" id="1_y0m7v"] 4 | [ext_resource type="Shader" uid="uid://dsww2pya7puex" path="res://addons/fusion_compute/examples/slime/display.gdshader" id="2_ur8uh"] 5 | 6 | [sub_resource type="ShaderMaterial" id="ShaderMaterial_nwdrs"] 7 | shader = ExtResource("2_ur8uh") 8 | 9 | [sub_resource type="Texture2DRD" id="Texture2DRD_42cko"] 10 | 11 | [node name="Control" type="Control"] 12 | layout_mode = 3 13 | anchors_preset = 15 14 | anchor_right = 1.0 15 | anchor_bottom = 1.0 16 | grow_horizontal = 2 17 | grow_vertical = 2 18 | script = ExtResource("1_y0m7v") 19 | evaporation = 0.8212 20 | diffusion = 0.0445 21 | trail_strength = 0.0468 22 | sensor_angle = 0.625 23 | sensor_distance = 16.4 24 | speed = 0.988 25 | turning = 0.3739 26 | random = 0.0309 27 | 28 | [node name="TextureRect" type="TextureRect" parent="."] 29 | texture_filter = 4 30 | material = SubResource("ShaderMaterial_nwdrs") 31 | layout_mode = 1 32 | anchors_preset = 15 33 | anchor_right = 1.0 34 | anchor_bottom = 1.0 35 | grow_horizontal = 2 36 | grow_vertical = 2 37 | texture = SubResource("Texture2DRD_42cko") 38 | expand_mode = 1 39 | stretch_mode = 1 40 | -------------------------------------------------------------------------------- /addons/fusion_compute/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Fusion Compute" 4 | description="A compute shader helper, made to make writing compute shaders less painful." 5 | author="SmallConfusion" 6 | version="1.3.1" 7 | script="" 8 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | import os 3 | 4 | build_file_name = "fusion-compute-plugin.zip" 5 | 6 | compress_type = zipfile.ZIP_DEFLATED 7 | compress_level = 9 8 | 9 | with zipfile.ZipFile( 10 | build_file_name, 11 | "w", 12 | compress_type, 13 | compresslevel=compress_level 14 | ) as zf: 15 | 16 | for root, dirs, files in os.walk("addons"): 17 | for file in files: 18 | filepath = os.path.join(root, file) 19 | zf.write(filepath, filepath) 20 | 21 | zf.write("README.md", os.path.join("addons", "fusion_compute", "README.md")) -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmallConfusion/Fusion-Compute/a43099ba90c38706ed58aa684a4b361b039e74f9/icon.png -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cr6wevsy2qbeo" 6 | path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.png" 14 | dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /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=5 10 | 11 | [application] 12 | 13 | config/name="Fusion Compute" 14 | run/main_scene="res://addons/fusion_compute/examples/slime/slime.tscn" 15 | config/features=PackedStringArray("4.4") 16 | config/icon="res://icon.png" 17 | 18 | [debug] 19 | 20 | settings/stdout/print_fps=true 21 | 22 | [display] 23 | 24 | window/size/viewport_width=1024 25 | window/size/viewport_height=1024 26 | window/stretch/aspect="ignore" 27 | window/vsync/vsync_mode=0 28 | 29 | [editor_plugins] 30 | 31 | enabled=PackedStringArray("res://addons/fusion_compute/plugin.cfg") 32 | 33 | [rendering] 34 | 35 | viewport/hdr_2d=true 36 | --------------------------------------------------------------------------------