diff --git a/misc/compute_shader_heightmap/README.md b/misc/compute_shader_heightmap/README.md new file mode 100644 index 00000000..4f2c587e --- /dev/null +++ b/misc/compute_shader_heightmap/README.md @@ -0,0 +1,34 @@ +# Compute Shader Heightmap + +This demo project gives an example of how to use *compute shaders* in Godot. +A compute shader is a piece of code that runs on the GPU and is written in GLSL +(as opposed to the +[Godot shader language](https://docs.godotengine.org/en/latest/tutorials/shaders/shader_reference/index.html)). + +A compute shader can be used to take advantage of the GPU's ability to perform +massively parallel operations faster than a CPU. This demo can generate the +heightmap of an island from a noise texture, both on the CPU and the GPU. You +can try both options to compare the time it takes to generate the heightmap on +the CPU and GPU respectively. + +For smaller noise textures, the CPU will often be faster, but the larger the +gains are by using the GPU. On a PC with a NVIDIA GeForce RTX 3060 and +11th-generation Intel Core i7 processor, the compute shader was tested to be +faster for textures 1024×1024 and larger. + +The dimensions of the image can be set on the exported **Dimensions** property +on the main scene. By default, it's set to 2048, which creates a 2048×2048 +heightmap. + +> **Note** +> +> The shader code has been structured to be followed step-by-step by the user, +> and may not necessarily represent best practices. The CPU code is also less +> optimized than it could be. This is to reflect the GPU code as much as +> possible. Besides the use of the GPU, no multithreading is used. + +Languages: GDScript, GLSL + +Renderer: Forward Mobile + +![Compute Shader Heightmap](screenshots/compute_shader_heightmap.webp) diff --git a/misc/compute_shader_heightmap/compute_shader.glsl b/misc/compute_shader_heightmap/compute_shader.glsl new file mode 100644 index 00000000..8a38b95e --- /dev/null +++ b/misc/compute_shader_heightmap/compute_shader.glsl @@ -0,0 +1,61 @@ +#[compute] +#version 460 + +// Instruct the GPU to use 8x8x1 = 64 local invocations per workgroup. +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +// Prepare memory for the image, which will be both read and written to +// `restrict` is used to tell the compiler that the memory will only be accessed +// by the `heightmap` variable. +layout(r8, binding = 0) restrict uniform image2D heightmap; +// `readonly` is used to tell the compiler that we will not write to this memory. +// This allows the compiler to make some optimizations it couldn't otherwise. +layout(rgba8, binding = 1) restrict readonly uniform image2D gradient; + +// This function is the GPU counterpart of `compute_island_cpu()` in `main.gd`. +void main() { + // Grab the current pixel's position from the ID of this specific invocation ("thread"). + ivec2 coords = ivec2(gl_GlobalInvocationID.xy); + ivec2 dimensions = imageSize(heightmap); + // Calculate the center of the image. + // Because we are working with integers ('round numbers') here, + // the result will be floored to an integer. + ivec2 center = dimensions / 2; + // Calculate the smallest distance from center to edge. + int smallest_radius = min(center.x, center.y); + + // Calculate the distance from the center of the image to the current pixel. + float dist = distance(coords, center); + // Retrieve the range of the gradient image. + int gradient_max_x = imageSize(gradient).x - 1; + // Calculate the gradient index based on the distance from the center. + // `mix()` functions similarly to `lerp()` in GDScript. + int gradient_x = int(mix(0.0, float(gradient_max_x), dist / float(smallest_radius))); + + // Retrieve the gradient value at the calculated position. + ivec2 gradient_pos = ivec2(gradient_x, 0); + vec4 gradient_color = imageLoad(gradient, gradient_pos); + + // Even though the image format only has the red channel, + // this will still return a vec4: `vec4(red, 0.0, 0.0, 1.0)` + vec4 pixel = imageLoad(heightmap, coords); + + // Multiply the pixel's red channel by the gradient's red channel + // (or any RGB channel, they're all the same except for alpha). + pixel.r *= gradient_color.r; + // If the pixel is below a certain threshold, this sets it to 0.0. + // The `step()` function is like `clamp()`, but it returns 0.0 if the value is + // below the threshold, or 1.0 if it is above. + // + // This is why we multiply it by the pixel's value again: to get the original + // value back if it is above the threshold. This shorthand replaces an `if` + // statement, which would cause branching and thus potentially slow down the + // shader. + pixel.r = step(0.2, pixel.r) * pixel.r; + + // Store the pixel back into the image. + // WARNING: make sure you are writing to the same coordinate that you read from. + // If you don't, you may end up writing to a pixel, before that pixel is read + // by a different invocation and cause errors. + imageStore(heightmap, coords, pixel); +} diff --git a/misc/compute_shader_heightmap/compute_shader.glsl.import b/misc/compute_shader_heightmap/compute_shader.glsl.import new file mode 100644 index 00000000..64e0cfba --- /dev/null +++ b/misc/compute_shader_heightmap/compute_shader.glsl.import @@ -0,0 +1,14 @@ +[remap] + +importer="glsl" +type="RDShaderFile" +uid="uid://cgowuwlmmweew" +path="res://.godot/imported/compute_shader.glsl-0816de56a4f9dce0934d88a29a0758cf.res" + +[deps] + +source_file="res://compute_shader.glsl" +dest_files=["res://.godot/imported/compute_shader.glsl-0816de56a4f9dce0934d88a29a0758cf.res"] + +[params] + diff --git a/misc/compute_shader_heightmap/icon.svg b/misc/compute_shader_heightmap/icon.svg new file mode 100644 index 00000000..adc26df6 --- /dev/null +++ b/misc/compute_shader_heightmap/icon.svg @@ -0,0 +1 @@ + diff --git a/misc/compute_shader_heightmap/icon.svg.import b/misc/compute_shader_heightmap/icon.svg.import new file mode 100644 index 00000000..e97b2d13 --- /dev/null +++ b/misc/compute_shader_heightmap/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://651nx11bd2yf" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/bptc_ldr=0 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/misc/compute_shader_heightmap/main.gd b/misc/compute_shader_heightmap/main.gd new file mode 100644 index 00000000..20fd7ecb --- /dev/null +++ b/misc/compute_shader_heightmap/main.gd @@ -0,0 +1,260 @@ +extends Control + +@export_file("*.glsl") var shader_file +@export_range(128, 4096, 1, "exp") var dimension: int = 512 + +@onready var seed_input: SpinBox = $CenterContainer/VBoxContainer/PanelContainer/VBoxContainer/GridContainer/SeedInput +@onready var heightmap_rect: TextureRect = $CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer/GridContainer/RawHeightmap +@onready var island_rect: TextureRect = $CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer/GridContainer/ComputedHeightmap + +var noise: FastNoiseLite +var gradient: Gradient +var gradient_tex: GradientTexture1D + +var po2_dimensions: int +var start_time: int + +var rd: RenderingDevice +var shader_rid: RID +var heightmap_rid: RID +var gradient_rid: RID +var uniform_set: RID +var pipeline: RID + +func _init() -> void: + # Create a noise function as the basis for our heightmap. + noise = FastNoiseLite.new() + noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH + noise.fractal_octaves = 5 + noise.fractal_lacunarity = 1.9 + + # Create a gradient to function as overlay. + gradient = Gradient.new() + gradient.add_point(0.6, Color(0.9, 0.9, 0.9, 1.0)) + gradient.add_point(0.8, Color(1.0, 1.0, 1.0, 1.0)) + # The gradient will start black, transition to grey in the first 70%, then to white in the last 30%. + gradient.reverse() + + # Create a 1D texture (single row of pixels) from gradient. + gradient_tex = GradientTexture1D.new() + gradient_tex.gradient = gradient + + +func _ready() -> void: + randomize_seed() + po2_dimensions = nearest_po2(dimension) + + noise.frequency = 0.003 / (float(po2_dimensions) / 512.0) + + # Append GPU and CPU model names to make performance comparison more informed. + # On unbalanced configurations where the CPU is much stronger than the GPU, + # compute shaders may not be beneficial. + $CenterContainer/VBoxContainer/PanelContainer/VBoxContainer/HBoxContainer/CreateButtonGPU.text += "\n" + RenderingServer.get_video_adapter_name() + $CenterContainer/VBoxContainer/PanelContainer/VBoxContainer/HBoxContainer/CreateButtonCPU.text += "\n" + OS.get_processor_name() + + +func _notification(what): + # Object destructor, triggered before the engine deletes this Node. + if what == NOTIFICATION_PREDELETE: + cleanup_gpu() + + +# Generate a random integer, convert it to a string and set it as text for the TextEdit field. +func randomize_seed() -> void: + seed_input.value = randi() + + +func prepare_image() -> Image: + start_time = Time.get_ticks_usec() + # Use the to_int() method on the String to convert to a valid seed. + noise.seed = seed_input.value + # Create image from noise. + var heightmap := noise.get_image(po2_dimensions, po2_dimensions, false, false) + + # Create ImageTexture to display original on screen. + var clone = Image.new() + clone.copy_from(heightmap) + clone.resize(512, 512, Image.INTERPOLATE_NEAREST) + var clone_tex := ImageTexture.create_from_image(clone) + heightmap_rect.texture = clone_tex + + return heightmap + + +func init_gpu() -> void: + # These resources are expensive to make, so create them once and cache for subsequent runs. + + # Create a local rendering device (required to run compute shaders). + rd = RenderingServer.create_local_rendering_device() + + if rd == null: + OS.alert("""Couldn't create local RenderingDevice on GPU: %s + +Note: RenderingDevice is only available in the Forward Plus and Forward Mobile backends, not Compatibility.""" % RenderingServer.get_video_adapter_name()) + return + + # Prepare the shader. + shader_rid = load_shader(rd, shader_file) + + # Create format for heightmap. + var heightmap_format := RDTextureFormat.new() + # There are a lot of different formats. It might take some studying to be able to be able to + # choose the right ones. In this case, we tell it to interpret the data as a single byte for red. + # Even though the noise image only has a luminance channel, we can just interpret this as if it + # was the red channel. The byte layout is the same! + heightmap_format.format = RenderingDevice.DATA_FORMAT_R8_UNORM + heightmap_format.width = po2_dimensions + heightmap_format.height = po2_dimensions + # The TextureUsageBits are stored as 'bit fields', denoting what can be done with the data. + # Because of how bit fields work, we can just sum the required ones: 8 + 64 + 128 + heightmap_format.usage_bits = \ + RenderingDevice.TEXTURE_USAGE_STORAGE_BIT + \ + RenderingDevice.TEXTURE_USAGE_CAN_UPDATE_BIT + \ + RenderingDevice.TEXTURE_USAGE_CAN_COPY_FROM_BIT + + # Prepare heightmap texture. We will set the data later. + heightmap_rid = rd.texture_create(heightmap_format, RDTextureView.new()) + + # Create uniform for heightmap. + var heightmap_uniform := RDUniform.new() + heightmap_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE + heightmap_uniform.binding = 0 # This matches the binding in the shader. + heightmap_uniform.add_id(heightmap_rid) + + # Create format for the gradient. + var gradient_format := RDTextureFormat.new() + # The gradient could have been converted to a single channel like we did with the heightmap, + # but for illustrative purposes, we use four channels (RGBA). + gradient_format.format = RenderingDevice.DATA_FORMAT_R8G8B8A8_UNORM + gradient_format.width = gradient_tex.width # Default: 256 + # GradientTexture1D always has a height of 1. + gradient_format.height = 1 + gradient_format.usage_bits = \ + RenderingDevice.TEXTURE_USAGE_STORAGE_BIT + \ + RenderingDevice.TEXTURE_USAGE_CAN_UPDATE_BIT + + # Storage gradient as texture. + gradient_rid = rd.texture_create(gradient_format, RDTextureView.new(), [gradient_tex.get_image().get_data()]) + + # Create uniform for gradient. + var gradient_uniform := RDUniform.new() + gradient_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE + gradient_uniform.binding = 1 # This matches the binding in the shader. + gradient_uniform.add_id(gradient_rid) + + uniform_set = rd.uniform_set_create([heightmap_uniform, gradient_uniform], shader_rid, 0) + + pipeline = rd.compute_pipeline_create(shader_rid) + + +func compute_island_gpu(heightmap: Image) -> void: + if rd == null: + init_gpu() + + if rd == null: + $CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer/HBoxContainer2/Label2.text = \ + "RenderingDevice is not available on the current rendering driver" + return + + # Store heightmap as texture. + rd.texture_update(heightmap_rid, 0, heightmap.get_data()) + + var compute_list := rd.compute_list_begin() + rd.compute_list_bind_compute_pipeline(compute_list, pipeline) + rd.compute_list_bind_uniform_set(compute_list, uniform_set, 0) + # This is where the magic happens! As our shader has a work group size of 8x8x1, we dispatch + # one for every 8x8 block of pixels here. This ratio is highly tunable, and performance may vary. + rd.compute_list_dispatch(compute_list, po2_dimensions / 8, po2_dimensions / 8, 1) + rd.compute_list_end() + + rd.submit() + # Wait for the GPU to finish. + # Normally, you would do this after a few frames have passed so the compute shader can run in the background. + rd.sync() + + # Retrieve processed data. + var output_bytes := rd.texture_get_data(heightmap_rid, 0) + # Even though the GPU was working on the image as if each byte represented the red channel, + # we'll interpret the data as if it was the luminance channel. + var island_img := Image.create_from_data(po2_dimensions, po2_dimensions, false, Image.FORMAT_L8, output_bytes) + + display_island(island_img) + + +func cleanup_gpu() -> void: + if rd == null: + return + + # All resources must be freed after use to avoid memory leaks. + + rd.free_rid(pipeline) + pipeline = RID() + + rd.free_rid(uniform_set) + uniform_set = RID() + + rd.free_rid(gradient_rid) + gradient_rid = RID() + + rd.free_rid(heightmap_rid) + heightmap_rid = RID() + + rd.free_rid(shader_rid) + shader_rid = RID() + + rd.free() + rd = null + + +# Import, compile and load shader, return reference. +func load_shader(rd: RenderingDevice, path: String) -> RID: + var shader_file_data: RDShaderFile = load(path) + var shader_spirv: RDShaderSPIRV = shader_file_data.get_spirv() + return rd.shader_create_from_spirv(shader_spirv) + + +func compute_island_cpu(heightmap: Image) -> void: + # This function is the CPU counterpart of the `main()` function in `compute_shader.glsl`. + var center := Vector2i(po2_dimensions, po2_dimensions) / 2 + # Loop over all pixel coordinates in the image. + for y in range(0, po2_dimensions): + for x in range(0, po2_dimensions): + var coord := Vector2i(x, y) + var pixel := heightmap.get_pixelv(coord) + # Calculate the distance between the coord and the center. + var distance := Vector2(center).distance_to(Vector2(coord)) + # As the X and Y dimensions are the same, we can use center.x as a proxy for the distance + # from the center to an edge. + var gradient_color := gradient.sample(distance / float(center.x)) + # We use the v ('value') of the pixel here. This is not the same as the luminance we use + # in the compute shader, but close enough for our purposes here. + pixel.v *= gradient_color.v + if pixel.v < 0.2: + pixel.v = 0.0 + heightmap.set_pixelv(coord, pixel) + display_island(heightmap) + + +func display_island(island: Image) -> void: + # Create ImageTexture to display original on screen. + var island_tex := ImageTexture.create_from_image(island) + island_rect.texture = island_tex + + # Calculate and display elapsed time. + var stop_time := Time.get_ticks_usec() + var elapsed := stop_time - start_time + $CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer/HBoxContainer/Label2.text = "%s ms" % str(elapsed * 0.001).pad_decimals(1) + + +func _on_random_button_pressed() -> void: + randomize_seed() + + +func _on_create_button_gpu_pressed() -> void: + var heightmap = prepare_image() + call_deferred("compute_island_gpu", heightmap) + + +func _on_create_button_cpu_pressed() -> void: + var heightmap = prepare_image() + call_deferred("compute_island_cpu", heightmap) diff --git a/misc/compute_shader_heightmap/main.tscn b/misc/compute_shader_heightmap/main.tscn new file mode 100644 index 00000000..36e29f6a --- /dev/null +++ b/misc/compute_shader_heightmap/main.tscn @@ -0,0 +1,158 @@ +[gd_scene load_steps=4 format=3 uid="uid://dm8doenormt4l"] + +[ext_resource type="Script" path="res://main.gd" id="1_r4h6n"] + +[sub_resource type="Gradient" id="Gradient_l3ffd"] +colors = PackedColorArray(0, 0.686275, 0.658824, 1, 0.542081, 0.741499, 1, 1) + +[sub_resource type="GradientTexture2D" id="GradientTexture2D_uwj8q"] +gradient = SubResource("Gradient_l3ffd") +fill_to = Vector2(1, 1) + +[node name="Main" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_r4h6n") +shader_file = "res://compute_shader.glsl" + +[node name="Background" type="TextureRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = SubResource("GradientTexture2D_uwj8q") + +[node name="CenterContainer" type="CenterContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="PanelContainer" type="PanelContainer" parent="CenterContainer/VBoxContainer"] +custom_minimum_size = Vector2(250, 0) +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/VBoxContainer/PanelContainer"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="TitleLabel" type="Label" parent="CenterContainer/VBoxContainer/PanelContainer/VBoxContainer"] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Create Island" +horizontal_alignment = 1 +vertical_alignment = 2 + +[node name="HSeparator" type="HSeparator" parent="CenterContainer/VBoxContainer/PanelContainer/VBoxContainer"] +layout_mode = 2 + +[node name="GridContainer" type="HBoxContainer" parent="CenterContainer/VBoxContainer/PanelContainer/VBoxContainer"] +layout_mode = 2 +alignment = 1 + +[node name="SeedLabel" type="Label" parent="CenterContainer/VBoxContainer/PanelContainer/VBoxContainer/GridContainer"] +layout_mode = 2 +size_flags_vertical = 1 +text = "Seed" +horizontal_alignment = 2 +vertical_alignment = 1 + +[node name="SeedInput" type="SpinBox" parent="CenterContainer/VBoxContainer/PanelContainer/VBoxContainer/GridContainer"] +custom_minimum_size = Vector2(140, 35) +layout_mode = 2 +max_value = 1e+12 + +[node name="RandomButton" type="Button" parent="CenterContainer/VBoxContainer/PanelContainer/VBoxContainer/GridContainer"] +layout_mode = 2 +text = "Random" + +[node name="HBoxContainer" type="HBoxContainer" parent="CenterContainer/VBoxContainer/PanelContainer/VBoxContainer"] +layout_mode = 2 +alignment = 1 + +[node name="CreateButtonGPU" type="Button" parent="CenterContainer/VBoxContainer/PanelContainer/VBoxContainer/HBoxContainer"] +custom_minimum_size = Vector2(0, 54) +layout_mode = 2 +size_flags_horizontal = 3 +text = "Create (GPU)" + +[node name="CreateButtonCPU" type="Button" parent="CenterContainer/VBoxContainer/PanelContainer/VBoxContainer/HBoxContainer"] +custom_minimum_size = Vector2(0, 54) +layout_mode = 2 +size_flags_horizontal = 3 +text = "Create (CPU)" + +[node name="PanelContainer2" type="PanelContainer" parent="CenterContainer/VBoxContainer"] +custom_minimum_size = Vector2(1034, 0) +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/VBoxContainer/PanelContainer2"] +layout_mode = 2 + +[node name="GridContainer" type="GridContainer" parent="CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer"] +layout_mode = 2 +theme_override_constants/v_separation = 10 +columns = 2 + +[node name="Label" type="Label" parent="CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer/GridContainer"] +custom_minimum_size = Vector2(0, 30) +layout_mode = 2 +text = "Raw Heightmap" +horizontal_alignment = 1 +vertical_alignment = 2 + +[node name="Label2" type="Label" parent="CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer/GridContainer"] +custom_minimum_size = Vector2(0, 30) +layout_mode = 2 +text = "Computed Island" +horizontal_alignment = 1 +vertical_alignment = 2 + +[node name="RawHeightmap" type="TextureRect" parent="CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer/GridContainer"] +custom_minimum_size = Vector2(384, 384) +layout_mode = 2 +size_flags_horizontal = 3 +ignore_texture_size = true +stretch_mode = 5 + +[node name="ComputedHeightmap" type="TextureRect" parent="CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer/GridContainer"] +custom_minimum_size = Vector2(384, 384) +layout_mode = 2 +size_flags_horizontal = 3 +ignore_texture_size = true +stretch_mode = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="Label" type="Label" parent="CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Time elapsed: " +horizontal_alignment = 2 +vertical_alignment = 1 + +[node name="Label2" type="Label" parent="CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer/HBoxContainer"] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(1, 0.941176, 0.47451, 1) +theme_override_font_sizes/font_size = 24 +text = "... ms" + +[connection signal="pressed" from="CenterContainer/VBoxContainer/PanelContainer/VBoxContainer/GridContainer/RandomButton" to="." method="_on_random_button_pressed"] +[connection signal="pressed" from="CenterContainer/VBoxContainer/PanelContainer/VBoxContainer/HBoxContainer/CreateButtonGPU" to="." method="_on_create_button_gpu_pressed"] +[connection signal="pressed" from="CenterContainer/VBoxContainer/PanelContainer/VBoxContainer/HBoxContainer/CreateButtonCPU" to="." method="_on_create_button_cpu_pressed"] diff --git a/misc/compute_shader_heightmap/project.godot b/misc/compute_shader_heightmap/project.godot new file mode 100644 index 00000000..a78330da --- /dev/null +++ b/misc/compute_shader_heightmap/project.godot @@ -0,0 +1,26 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Compute Shader Heightmap" +run/main_scene="res://main.tscn" +config/features=PackedStringArray("4.0") +run/low_processor_mode=true +config/icon="res://icon.svg" + +[display] + +window/stretch/mode="canvas_items" +window/stretch/aspect="expand" + +[rendering] + +renderer/rendering_method="mobile" diff --git a/misc/compute_shader_heightmap/screenshots/.gdignore b/misc/compute_shader_heightmap/screenshots/.gdignore new file mode 100644 index 00000000..e69de29b diff --git a/misc/compute_shader_heightmap/screenshots/compute_shader_heightmap.webp b/misc/compute_shader_heightmap/screenshots/compute_shader_heightmap.webp new file mode 100644 index 00000000..a250c9bd Binary files /dev/null and b/misc/compute_shader_heightmap/screenshots/compute_shader_heightmap.webp differ