Files
fps_project_1/addons/proton_scatter/src/modifiers/relax.gd

192 lines
6.3 KiB
GDScript

@tool
extends "base_modifier.gd"
static var shader_file: RDShaderFile
@export var iterations : int = 3
@export var offset_step : float = 0.01
@export var consecutive_step_multiplier : float = 0.5
@export var use_computeshader : bool = true
func _init() -> void:
display_name = "Relax Position"
category = "Edit"
global_reference_frame_available = false
local_reference_frame_available = false
individual_instances_reference_frame_available = false
can_restrict_height = true
restrict_height = true
documentation.add_warning(
"This modifier is has an O(n²) complexity and will be slow with
large amounts of points, unless your device supports compute shaders.",
1)
var p := documentation.add_parameter("iterations")
p.set_type("int")
p.set_cost(2)
p.set_description(
"How many times the relax algorithm will run. Increasing this value will
generally improves the result, at the cost of execution speed."
)
p = documentation.add_parameter("Offset step")
p.set_type("float")
p.set_cost(0)
p.set_description("How far the transform will be pushed away each iteration.")
p = documentation.add_parameter("Consecutive step multiplier")
p.set_type("float")
p.set_cost(0)
p.set_description(
"On each iteration, multiply the offset step by this value. This value
is usually set between 0 and 1, to make the effect less pronounced on
successive iterations.")
p = documentation.add_parameter("Use compute shader")
p.set_cost(0)
p.set_type("bool")
p.set_description(
"Run the calculations on the GPU instead of the CPU. This provides
a significant speed boost and should be enabled when possible.")
p.add_warning(
"This parameter can't be enabled when using the OpenGL backend or running
in headless mode.", 2)
func _process_transforms(transforms, _domain, _seed) -> void:
var offset := offset_step
if transforms.size() < 2:
return
# Disable the use of compute shader, if we cannot create a RenderingDevice
if use_computeshader:
var rd := RenderingServer.create_local_rendering_device()
if rd == null:
use_computeshader = false
else:
rd.free()
rd = null
if use_computeshader:
for iteration in iterations:
if interrupt_update:
return
var movedir: PackedVector3Array = compute_closest(transforms)
for i in transforms.size():
var dir = movedir[i]
if restrict_height:
dir.y = 0.0
# move away from closest point
transforms.list[i].origin += dir.normalized() * offset
offset *= consecutive_step_multiplier
else:
# calculate the relax transforms on the cpu
for iteration in iterations:
for i in transforms.size():
if interrupt_update:
return
var min_vector = Vector3.ONE * 99999.0
var threshold := 99999.0
var distance := 0.0
var diff: Vector3
# Find the closest point
for j in transforms.size():
if i == j:
continue
diff = transforms.list[i].origin - transforms.list[j].origin
distance = diff.length_squared()
if distance < threshold:
min_vector = diff
threshold = distance
if restrict_height:
min_vector.y = 0.0
# move away from closest point
transforms.list[i].origin += min_vector.normalized() * offset
offset *= consecutive_step_multiplier
# compute the closest points to each other using a compute shader
# return a vector for each point that points away from the closest neighbour
func compute_closest(transforms) -> PackedVector3Array:
var padded_num_vecs = ceil(float(transforms.size()) / 64.0) * 64
var padded_num_floats = padded_num_vecs * 4
var rd := RenderingServer.create_local_rendering_device()
var shader_spirv: RDShaderSPIRV = get_shader_file().get_spirv()
var shader := rd.shader_create_from_spirv(shader_spirv)
# Prepare our data. We use vec4 floats in the shader, so we need 32 bit.
var input := PackedFloat32Array()
for i in transforms.size():
input.append(transforms.list[i].origin.x)
input.append(transforms.list[i].origin.y)
input.append(transforms.list[i].origin.z)
input.append(0) # needed to use vec4, necessary for byte alignment in the shader code
# buffer size, number of vectors sent to the gpu
input.resize(padded_num_floats) # indexing in the compute shader requires padding
var input_bytes := input.to_byte_array()
var output_bytes := input_bytes.duplicate()
# Create a storage buffer that can hold our float values.
var buffer_in := rd.storage_buffer_create(input_bytes.size(), input_bytes)
var buffer_out := rd.storage_buffer_create(output_bytes.size(), output_bytes)
# Create a uniform to assign the buffer to the rendering device
var uniform_in := RDUniform.new()
uniform_in.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
uniform_in.binding = 0 # this needs to match the "binding" in our shader file
uniform_in.add_id(buffer_in)
# Create a uniform to assign the buffer to the rendering device
var uniform_out := RDUniform.new()
uniform_out.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
uniform_out.binding = 1 # this needs to match the "binding" in our shader file
uniform_out.add_id(buffer_out)
# the last parameter (the 0) needs to match the "set" in our shader file
var uniform_set := rd.uniform_set_create([uniform_in, uniform_out], shader, 0)
# Create a compute pipeline
var pipeline := rd.compute_pipeline_create(shader)
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)
# each workgroup computes 64 vectors
# print("Dispatching workgroups: ", padded_num_vecs/64)
rd.compute_list_dispatch(compute_list, padded_num_vecs/64, 1, 1)
rd.compute_list_end()
# Submit to GPU and wait for sync
rd.submit()
rd.sync()
# Read back the data from the buffer
var result_bytes := rd.buffer_get_data(buffer_out)
var result := result_bytes.to_float32_array()
var retval = PackedVector3Array()
for i in transforms.size():
retval.append(Vector3(result[i*4], result[i*4+1], result[i*4+2]))
# Free the allocated objects.
# All resources must be freed after use to avoid memory leaks.
if rd != null:
rd.free_rid(pipeline)
rd.free_rid(uniform_set)
rd.free_rid(shader)
rd.free_rid(buffer_in)
rd.free_rid(buffer_out)
rd.free()
rd = null
return retval
func get_shader_file() -> RDShaderFile:
if shader_file == null:
shader_file = load(get_script().resource_path.get_base_dir() + "/compute_shaders/compute_relax.glsl")
return shader_file