main.gd 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. extends Control
  2. @export_file("*.glsl") var shader_file
  3. @export_range(128, 4096, 1, "exp") var dimension: int = 512
  4. @onready var seed_input: SpinBox = $CenterContainer/VBoxContainer/PanelContainer/VBoxContainer/GridContainer/SeedInput
  5. @onready var heightmap_rect: TextureRect = $CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer/GridContainer/RawHeightmap
  6. @onready var island_rect: TextureRect = $CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer/GridContainer/ComputedHeightmap
  7. var noise: FastNoiseLite
  8. var gradient: Gradient
  9. var gradient_tex: GradientTexture1D
  10. var po2_dimensions: int
  11. var start_time: int
  12. var rd: RenderingDevice
  13. var shader_rid: RID
  14. var heightmap_rid: RID
  15. var gradient_rid: RID
  16. var uniform_set: RID
  17. var pipeline: RID
  18. func _init() -> void:
  19. # Create a noise function as the basis for our heightmap.
  20. noise = FastNoiseLite.new()
  21. noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
  22. noise.fractal_octaves = 5
  23. noise.fractal_lacunarity = 1.9
  24. # Create a gradient to function as overlay.
  25. gradient = Gradient.new()
  26. gradient.add_point(0.6, Color(0.9, 0.9, 0.9, 1.0))
  27. gradient.add_point(0.8, Color(1.0, 1.0, 1.0, 1.0))
  28. # The gradient will start black, transition to grey in the first 70%, then to white in the last 30%.
  29. gradient.reverse()
  30. # Create a 1D texture (single row of pixels) from gradient.
  31. gradient_tex = GradientTexture1D.new()
  32. gradient_tex.gradient = gradient
  33. func _ready() -> void:
  34. randomize_seed()
  35. po2_dimensions = nearest_po2(dimension)
  36. noise.frequency = 0.003 / (float(po2_dimensions) / 512.0)
  37. # Append GPU and CPU model names to make performance comparison more informed.
  38. # On unbalanced configurations where the CPU is much stronger than the GPU,
  39. # compute shaders may not be beneficial.
  40. $CenterContainer/VBoxContainer/PanelContainer/VBoxContainer/HBoxContainer/CreateButtonGPU.text += "\n" + RenderingServer.get_video_adapter_name()
  41. $CenterContainer/VBoxContainer/PanelContainer/VBoxContainer/HBoxContainer/CreateButtonCPU.text += "\n" + OS.get_processor_name()
  42. func _notification(what):
  43. # Object destructor, triggered before the engine deletes this Node.
  44. if what == NOTIFICATION_PREDELETE:
  45. cleanup_gpu()
  46. # Generate a random integer, convert it to a string and set it as text for the TextEdit field.
  47. func randomize_seed() -> void:
  48. seed_input.value = randi()
  49. func prepare_image() -> Image:
  50. start_time = Time.get_ticks_usec()
  51. # Use the to_int() method on the String to convert to a valid seed.
  52. noise.seed = seed_input.value
  53. # Create image from noise.
  54. var heightmap := noise.get_image(po2_dimensions, po2_dimensions, false, false)
  55. # Create ImageTexture to display original on screen.
  56. var clone = Image.new()
  57. clone.copy_from(heightmap)
  58. clone.resize(512, 512, Image.INTERPOLATE_NEAREST)
  59. var clone_tex := ImageTexture.create_from_image(clone)
  60. heightmap_rect.texture = clone_tex
  61. return heightmap
  62. func init_gpu() -> void:
  63. # These resources are expensive to make, so create them once and cache for subsequent runs.
  64. # Create a local rendering device (required to run compute shaders).
  65. rd = RenderingServer.create_local_rendering_device()
  66. if rd == null:
  67. OS.alert("""Couldn't create local RenderingDevice on GPU: %s
  68. Note: RenderingDevice is only available in the Forward Plus and Forward Mobile backends, not Compatibility.""" % RenderingServer.get_video_adapter_name())
  69. return
  70. # Prepare the shader.
  71. shader_rid = load_shader(rd, shader_file)
  72. # Create format for heightmap.
  73. var heightmap_format := RDTextureFormat.new()
  74. # There are a lot of different formats. It might take some studying to be able to be able to
  75. # choose the right ones. In this case, we tell it to interpret the data as a single byte for red.
  76. # Even though the noise image only has a luminance channel, we can just interpret this as if it
  77. # was the red channel. The byte layout is the same!
  78. heightmap_format.format = RenderingDevice.DATA_FORMAT_R8_UNORM
  79. heightmap_format.width = po2_dimensions
  80. heightmap_format.height = po2_dimensions
  81. # The TextureUsageBits are stored as 'bit fields', denoting what can be done with the data.
  82. # Because of how bit fields work, we can just sum the required ones: 8 + 64 + 128
  83. heightmap_format.usage_bits = \
  84. RenderingDevice.TEXTURE_USAGE_STORAGE_BIT + \
  85. RenderingDevice.TEXTURE_USAGE_CAN_UPDATE_BIT + \
  86. RenderingDevice.TEXTURE_USAGE_CAN_COPY_FROM_BIT
  87. # Prepare heightmap texture. We will set the data later.
  88. heightmap_rid = rd.texture_create(heightmap_format, RDTextureView.new())
  89. # Create uniform for heightmap.
  90. var heightmap_uniform := RDUniform.new()
  91. heightmap_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
  92. heightmap_uniform.binding = 0 # This matches the binding in the shader.
  93. heightmap_uniform.add_id(heightmap_rid)
  94. # Create format for the gradient.
  95. var gradient_format := RDTextureFormat.new()
  96. # The gradient could have been converted to a single channel like we did with the heightmap,
  97. # but for illustrative purposes, we use four channels (RGBA).
  98. gradient_format.format = RenderingDevice.DATA_FORMAT_R8G8B8A8_UNORM
  99. gradient_format.width = gradient_tex.width # Default: 256
  100. # GradientTexture1D always has a height of 1.
  101. gradient_format.height = 1
  102. gradient_format.usage_bits = \
  103. RenderingDevice.TEXTURE_USAGE_STORAGE_BIT + \
  104. RenderingDevice.TEXTURE_USAGE_CAN_UPDATE_BIT
  105. # Storage gradient as texture.
  106. gradient_rid = rd.texture_create(gradient_format, RDTextureView.new(), [gradient_tex.get_image().get_data()])
  107. # Create uniform for gradient.
  108. var gradient_uniform := RDUniform.new()
  109. gradient_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
  110. gradient_uniform.binding = 1 # This matches the binding in the shader.
  111. gradient_uniform.add_id(gradient_rid)
  112. uniform_set = rd.uniform_set_create([heightmap_uniform, gradient_uniform], shader_rid, 0)
  113. pipeline = rd.compute_pipeline_create(shader_rid)
  114. func compute_island_gpu(heightmap: Image) -> void:
  115. if rd == null:
  116. init_gpu()
  117. if rd == null:
  118. $CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer/HBoxContainer2/Label2.text = \
  119. "RenderingDevice is not available on the current rendering driver"
  120. return
  121. # Store heightmap as texture.
  122. rd.texture_update(heightmap_rid, 0, heightmap.get_data())
  123. var compute_list := rd.compute_list_begin()
  124. rd.compute_list_bind_compute_pipeline(compute_list, pipeline)
  125. rd.compute_list_bind_uniform_set(compute_list, uniform_set, 0)
  126. # This is where the magic happens! As our shader has a work group size of 8x8x1, we dispatch
  127. # one for every 8x8 block of pixels here. This ratio is highly tunable, and performance may vary.
  128. rd.compute_list_dispatch(compute_list, po2_dimensions / 8, po2_dimensions / 8, 1)
  129. rd.compute_list_end()
  130. rd.submit()
  131. # Wait for the GPU to finish.
  132. # Normally, you would do this after a few frames have passed so the compute shader can run in the background.
  133. rd.sync()
  134. # Retrieve processed data.
  135. var output_bytes := rd.texture_get_data(heightmap_rid, 0)
  136. # Even though the GPU was working on the image as if each byte represented the red channel,
  137. # we'll interpret the data as if it was the luminance channel.
  138. var island_img := Image.create_from_data(po2_dimensions, po2_dimensions, false, Image.FORMAT_L8, output_bytes)
  139. display_island(island_img)
  140. func cleanup_gpu() -> void:
  141. if rd == null:
  142. return
  143. # All resources must be freed after use to avoid memory leaks.
  144. rd.free_rid(pipeline)
  145. pipeline = RID()
  146. rd.free_rid(uniform_set)
  147. uniform_set = RID()
  148. rd.free_rid(gradient_rid)
  149. gradient_rid = RID()
  150. rd.free_rid(heightmap_rid)
  151. heightmap_rid = RID()
  152. rd.free_rid(shader_rid)
  153. shader_rid = RID()
  154. rd.free()
  155. rd = null
  156. # Import, compile and load shader, return reference.
  157. func load_shader(rd: RenderingDevice, path: String) -> RID:
  158. var shader_file_data: RDShaderFile = load(path)
  159. var shader_spirv: RDShaderSPIRV = shader_file_data.get_spirv()
  160. return rd.shader_create_from_spirv(shader_spirv)
  161. func compute_island_cpu(heightmap: Image) -> void:
  162. # This function is the CPU counterpart of the `main()` function in `compute_shader.glsl`.
  163. var center := Vector2i(po2_dimensions, po2_dimensions) / 2
  164. # Loop over all pixel coordinates in the image.
  165. for y in range(0, po2_dimensions):
  166. for x in range(0, po2_dimensions):
  167. var coord := Vector2i(x, y)
  168. var pixel := heightmap.get_pixelv(coord)
  169. # Calculate the distance between the coord and the center.
  170. var distance := Vector2(center).distance_to(Vector2(coord))
  171. # As the X and Y dimensions are the same, we can use center.x as a proxy for the distance
  172. # from the center to an edge.
  173. var gradient_color := gradient.sample(distance / float(center.x))
  174. # We use the v ('value') of the pixel here. This is not the same as the luminance we use
  175. # in the compute shader, but close enough for our purposes here.
  176. pixel.v *= gradient_color.v
  177. if pixel.v < 0.2:
  178. pixel.v = 0.0
  179. heightmap.set_pixelv(coord, pixel)
  180. display_island(heightmap)
  181. func display_island(island: Image) -> void:
  182. # Create ImageTexture to display original on screen.
  183. var island_tex := ImageTexture.create_from_image(island)
  184. island_rect.texture = island_tex
  185. # Calculate and display elapsed time.
  186. var stop_time := Time.get_ticks_usec()
  187. var elapsed := stop_time - start_time
  188. $CenterContainer/VBoxContainer/PanelContainer2/VBoxContainer/HBoxContainer/Label2.text = "%s ms" % str(elapsed * 0.001).pad_decimals(1)
  189. func _on_random_button_pressed() -> void:
  190. randomize_seed()
  191. func _on_create_button_gpu_pressed() -> void:
  192. var heightmap = prepare_image()
  193. call_deferred("compute_island_gpu", heightmap)
  194. func _on_create_button_cpu_pressed() -> void:
  195. var heightmap = prepare_image()
  196. call_deferred("compute_island_cpu", heightmap)