Browse Source

Add a compute shader demo (#810)

Co-authored-by: dzil123 <5725958+dzil123@users.noreply.github.com>
Co-authored-by: Hugo Locurcio <hugo.locurcio@hugo.pro>
Co-authored-by: MoltenCoffee <13321277+MoltenCoffee@users.noreply.github.com>
Hugo Locurcio 2 years ago
parent
commit
85ca2fb2a1

+ 34 - 0
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)

+ 61 - 0
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);
+}

+ 14 - 0
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]
+

File diff suppressed because it is too large
+ 0 - 0
misc/compute_shader_heightmap/icon.svg


+ 37 - 0
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

+ 260 - 0
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)

+ 158 - 0
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"]

+ 26 - 0
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"

+ 0 - 0
misc/compute_shader_heightmap/screenshots/.gdignore


BIN
misc/compute_shader_heightmap/screenshots/compute_shader_heightmap.webp


Some files were not shown because too many files changed in this diff