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

132 lines
4.1 KiB
GDScript

@tool
extends "base_modifier.gd"
@export_file("Texture") var mask: String
@export var mask_rotation := 0.0
@export var mask_offset := Vector2.ZERO
@export var mask_scale := Vector2.ONE
@export var pixel_to_unit_ratio := 64.0
@export_range(0.0, 1.0) var remove_below = 0.1
@export_range(0.0, 1.0) var remove_above = 1.0
@export var scale_transforms := true
func _init() -> void:
display_name = "Clusterize"
category = "Edit"
global_reference_frame_available = true
local_reference_frame_available = false # TODO, enable this and handle this case
individual_instances_reference_frame_available = false
documentation.add_paragraph(
"Clump transforms together based on a mask.
Sampling the mask returns values between 0 and 1. The transforms are
scaled against these values which means, bright areas don't affect their
scale while dark area scales them down. Transforms are then removed
below a threshold, leaving clumps behind.")
var p := documentation.add_parameter("Mask")
p.set_type("Texture")
p.set_description("The texture used as a mask.")
p.add_warning(
"The amount of texture fetch depends on the amount of transforms
generated in the previous modifiers (4 reads for each transform).
In theory, the texture size shouldn't affect performances in a
noticeable way.")
p = documentation.add_parameter("Mask scale")
p.set_type("Vector2")
p.set_description(
"Depending on the mask resolution, the perceived scale will change.
Use this parameter to increase or decrease the area covered by the mask.")
p = documentation.add_parameter("Mask offset")
p.set_type("Vector2")
p.set_description("Moves the mask XZ position in 3D space")
p = documentation.add_parameter("Mask rotation")
p.set_type("Float")
p.set_description("Rotates the mask around the Y axis. (Angle in degrees)")
p = documentation.add_parameter("Remove below")
p.set_type("Float")
p.set_description("Threshold below which the transforms are removed.")
p = documentation.add_parameter("Remove above")
p.set_type("Float")
p.set_description("Threshold above which the transforms are removed.")
func _process_transforms(transforms, domain, _seed) -> void:
if not ResourceLoader.exists(mask):
warning += "The specified file " + mask + " could not be loaded."
return
var texture: Texture = load(mask)
if not texture is Texture:
warning += "The specified file is not a valid texture."
return
var image: Image
# Wait for a frame or risk the whole editor to freeze because of get_image()
# TODO: Check if more safe guards are required here.
await domain.get_root().get_tree().process_frame
if texture is Texture2D:
image = texture.get_image()
elif texture is Texture3D:
image = texture.get_data()[0] # TMP, this should depends on the transforms Y coordinates
elif texture is TextureLayered:
image = texture.get_layer_data(0) # TMP
image.decompress()
var width := image.get_width()
var height := image.get_height()
var i := 0
var angle := deg_to_rad(mask_rotation)
while i < transforms.list.size():
var t: Transform3D = transforms.list[i]
var origin := t.origin.rotated(Vector3.UP, angle)
var x := origin.x * (pixel_to_unit_ratio / mask_scale.x) + mask_offset.x
x = fposmod(x, width - 1)
var y := origin.z * (pixel_to_unit_ratio / mask_scale.y) + mask_offset.y
y = fposmod(y, height - 1)
var level := _get_pixel(image, x, y)
if level < remove_below:
transforms.list.remove_at(i)
continue
if level > remove_above:
transforms.list.remove_at(i)
continue
if scale_transforms:
t.basis = t.basis.scaled(Vector3(level, level, level))
transforms.list[i] = t
i += 1
# x and y don't always match an exact pixel, so we sample the neighboring
# pixels as well and return a weighted value based on the input coords.
func _get_pixel(image: Image, x: float, y: float) -> float:
var ix = int(x)
var iy = int(y)
x -= ix
y -= iy
var nw = image.get_pixel(ix, iy).v
var ne = image.get_pixel(ix + 1, iy).v
var sw = image.get_pixel(ix, iy + 1).v
var se = image.get_pixel(ix + 1, iy + 1).v
return nw * (1 - x) * (1 - y) + ne * x * (1 - y) + sw * (1 - x) * y + se * x * y