132 lines
4.1 KiB
GDScript
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
|