Added footsteps, new tree, various other tweaks

This commit is contained in:
derek
2024-12-05 11:47:34 -06:00
parent 816ae85938
commit 023879ea9f
389 changed files with 20484 additions and 234 deletions

View File

@@ -0,0 +1,182 @@
@tool
extends "base_modifier.gd"
# Takes existing objects and duplicates them recursively with given transforms
@export var amount := 1
@export var min_amount := -1
@export var local_offset := false
@export var offset := Vector3.ZERO
@export var local_rotation := false
@export var rotation := Vector3.ZERO
@export var individual_rotation_pivots := true
@export var rotation_pivot := Vector3.ZERO
@export var local_scale := true
@export var scale := Vector3.ONE
@export var randomize_indices := true
var _rng: RandomNumberGenerator
func _init() -> void:
display_name = "Array"
category = "Create"
can_override_seed = true
can_restrict_height = false
global_reference_frame_available = false
local_reference_frame_available = false
individual_instances_reference_frame_available = false
documentation.add_paragraph(
"Recursively creates copies of the existing transforms, with each copy
being offset from the previous one in any of a number of possible ways.")
var p := documentation.add_parameter("Amount")
p.set_type("int")
p.set_cost(2)
p.set_description(
"The iteration count. If set to 1, each existing transforms are copied
once.")
p.add_warning("If set to 0, no copies are created.")
p = documentation.add_parameter("Minimum amount")
p.set_type("int")
p.set_description(
"Creates a random amount of copies for each transforms, between this
value and the amount value.")
p.add_warning("Ignored if set to a negative value.")
p = documentation.add_parameter("Offset")
p.set_type("Vector3")
p.set_description(
"Adds a constant offset between each copies and the previous one.")
p = documentation.add_parameter("Local offset")
p.set_type("bool")
p.set_description(
"If enabled, offset is relative to the previous copy orientation.
Otherwise, the offset is in global space.")
p = documentation.add_parameter("Rotation")
p.set_type("Vector3")
p.set_description(
"The rotation offset (on each axes) to add on each copy.")
p = documentation.add_parameter("Local rotation")
p.set_type("bool")
p.set_description(
"If enabled, the rotation is applied in local space relative to each
individual transforms. Otherwise, the rotation is applied in global
space.")
p = documentation.add_parameter("Rotation Pivot")
p.set_type("Vector3")
p.set_description(
"The point around which each copies are rotated. By default, each
transforms are rotated around their individual centers.")
p = documentation.add_parameter("Individual Rotation Pivots")
p.set_type("bool")
p.set_description(
"If enabled, each copies will use their own pivot relative to the
previous copy. Otherwise, a single pivot point (defined in global space)
will be used for the rotation of [b]all[/b] the copies.")
p = documentation.add_parameter("Scale")
p.set_type("Vector3")
p.set_description(
"Scales the copies relative to the transforms they are from.")
p = documentation.add_parameter("Local Scale")
p.set_type("bool")
p.set_description(
"If enabled, scaling is applied in local space relative to each
individual transforms. Otherwise, global axes are used, resulting
in skewed transforms in most cases.")
p = documentation.add_parameter("Randomize Indices")
p.set_type("bool")
p.set_description(
"Randomize the transform list order. This is only useful to break up the
repetitive patterns if you're using multiple ScatterItem nodes.")
func _process_transforms(transforms, domain, random_seed: int) -> void:
_rng = RandomNumberGenerator.new()
_rng.set_seed(random_seed)
var new_transforms: Array[Transform3D] = []
var rotation_rad := Vector3.ZERO
rotation_rad.x = deg_to_rad(rotation.x)
rotation_rad.y = deg_to_rad(rotation.y)
rotation_rad.z = deg_to_rad(rotation.z)
var axis_x := Vector3.RIGHT
var axis_y := Vector3.UP
var axis_z := Vector3.FORWARD
for t in transforms.size():
new_transforms.push_back(transforms.list[t])
var steps = amount
if min_amount >= 0:
steps = _rng.randi_range(min_amount, amount)
for a in steps:
a += 1
# use original object's transform as base transform
var transform : Transform3D = transforms.list[t]
var basis := transform.basis
# first move to rotation point defined in rotation offset
var rotation_pivot_offset = rotation_pivot
if individual_rotation_pivots:
rotation_pivot_offset = transform * rotation_pivot
transform.origin -= rotation_pivot_offset
# then rotate
if local_rotation:
axis_x = basis.x.normalized()
axis_y = basis.y.normalized()
axis_z = basis.z.normalized()
transform = transform.rotated(axis_x, rotation_rad.x * a)
transform = transform.rotated(axis_y, rotation_rad.y * a)
transform = transform.rotated(axis_z, rotation_rad.z * a)
# scale
# If the scale is different than 1, each transform gets bigger or
# smaller for each iteration.
var s = scale
s.x = pow(s.x, a)
s.y = pow(s.y, a)
s.z = pow(s.z, a)
if local_scale:
transform.basis.x *= s.x
transform.basis.y *= s.y
transform.basis.z *= s.z
else:
transform.basis = transform.basis.scaled(s)
# apply changes back to the transform and undo the rotation pivot offset
transform.origin += rotation_pivot_offset
# offset
if local_offset:
transform.origin += offset * a
else:
transform.origin += (basis * offset) * a
# store the final result if the position is valid
if not domain.is_point_excluded(transform.origin):
new_transforms.push_back(transform)
transforms.list = new_transforms
if randomize_indices:
transforms.shuffle(random_seed)

View File

@@ -0,0 +1,126 @@
@tool
class_name ScatterBaseModifier
extends Resource
# Modifiers place transforms. They create, edit or remove transforms in a list,
# before the next Modifier in the stack does the same.
# All Modifiers must inherit from this class.
# Transforms in the provided transforms list must be in global space.
signal warning_changed
signal modifier_changed
const TransformList = preload("../common/transform_list.gd")
const Domain = preload("../common/domain.gd")
const DocumentationInfo = preload("../documentation/documentation_info.gd")
@export var enabled := true
@export var override_global_seed := false
@export var custom_seed := 0
@export var restrict_height := false # Tells the modifier whether to constrain transforms to the local XY plane or not
@export var reference_frame := 0
var display_name: String = "Base Modifier Name"
var category: String = "None"
var documentation := DocumentationInfo.new()
var warning: String = ""
var warning_ignore_no_transforms := false
var warning_ignore_no_shape := true
var expanded := false
var can_override_seed := false
var can_restrict_height := true
var global_reference_frame_available := true
var local_reference_frame_available := false
var individual_instances_reference_frame_available := false
var use_edge_data := false
var deprecated := false
var deprecation_message: String
var interrupt_update: bool = false
func get_warning() -> String:
return warning
func process_transforms(transforms: TransformList, domain: Domain, global_seed: int) -> void:
if not domain.get_root().is_inside_tree():
return
if Engine.is_editor_hint():
_clear_warning()
if deprecated:
warning += "This modifier is deprecated.\n"
warning += deprecation_message + "\n"
if not enabled:
warning_changed.emit()
return
if domain.is_empty() and not warning_ignore_no_shape:
warning += """The Scatter node does not have a shape.
Add at least one ScatterShape node as a child.\n"""
if transforms.is_empty() and not warning_ignore_no_transforms:
warning += """There's no transforms to act on.
Make sure you have a Create modifier before this one.\n
"""
var random_seed: int = global_seed
if can_override_seed and override_global_seed:
random_seed = custom_seed
interrupt_update = false
@warning_ignore("redundant_await") # Not redundant as child classes could use the await keyword here.
await _process_transforms(transforms, domain, random_seed)
warning_changed.emit()
func get_copy():
var script: Script = get_script()
var copy = script.new()
for p in get_property_list():
var value = get(p.name)
copy.set(p.name, value)
return copy
## Notify the modifier it should stop updating as soon as it can.
func interrupt() -> void:
interrupt_update = true
func is_using_global_space() -> bool:
return reference_frame == 0
func is_using_local_space() -> bool:
return reference_frame == 1
func is_using_individual_instances_space() -> bool:
return reference_frame == 2
func use_global_space_by_default() -> void:
reference_frame = 0
func use_local_space_by_default() -> void:
reference_frame = 1
func use_individual_instances_space_by_default() -> void:
reference_frame = 2
func _clear_warning() -> void:
warning = ""
warning_changed.emit()
# Override in inherited class
func _process_transforms(_transforms: TransformList, _domain: Domain, _seed: int) -> void:
pass

View File

@@ -0,0 +1,131 @@
@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

View File

@@ -0,0 +1,43 @@
#[compute]
#version 450
// Invocations in the (x, y, z) dimension
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
// A binding to the input buffer we create in our script
layout(set = 0, binding = 0, std430) readonly buffer BufferIn {
vec4 data[];
}
buffer_in;
// A binding to the output buffer we create in our script
layout(set = 0, binding = 1, std430) restrict buffer BufferOut {
vec4 data[];
}
buffer_out;
// The code we want to execute in each invocation
void main() {
int last_element_index = buffer_in.data.length();
// Unique index for each element
uint workgroupSize = gl_WorkGroupSize.x * gl_WorkGroupSize.y * gl_WorkGroupSize.z;
uint index = gl_WorkGroupID.x * workgroupSize + gl_LocalInvocationIndex;
vec3 infvec = vec3(1, 1, 1) * 999999; // vector approaching "infinity"
vec3 closest = infvec; // initialize closest to infinity
vec3 origin = buffer_in.data[index].xyz;
for(int i = 0; i <= last_element_index; i++){
vec3 newvec = buffer_in.data[i].xyz;
if (i == index) continue; // ignore self
float olddist = length(closest - origin);
float newdist = length(newvec - origin);
if (newdist < olddist)
{
closest = newvec;
}
}
buffer_out.data[index] = vec4(origin - closest, 0);
}

View File

@@ -0,0 +1,14 @@
[remap]
importer="glsl"
type="RDShaderFile"
uid="uid://cpg67dxgr360g"
path="res://.godot/imported/compute_relax.glsl-b06f9e60cda7719b78bde9673f2501b7.res"
[deps]
source_file="res://addons/proton_scatter/src/modifiers/compute_shaders/compute_relax.glsl"
dest_files=["res://.godot/imported/compute_relax.glsl-b06f9e60cda7719b78bde9673f2501b7.res"]
[params]

View File

@@ -0,0 +1,98 @@
@tool
extends "base_modifier.gd"
@export var item_length := 2.0
@export var ignore_slopes := false
var _current_offset = 0.0
func _init() -> void:
display_name = "Create Along Edge (Continuous)"
category = "Create"
warning_ignore_no_transforms = true
warning_ignore_no_shape = false
use_edge_data = true
global_reference_frame_available = false
local_reference_frame_available = false
individual_instances_reference_frame_available = false
var p
documentation.add_paragraph(
"Create new transforms along the edges of the Scatter shapes. These
transforms are placed so they touch each other but don't overlap, even
if the curve has sharp turns.")
documentation.add_paragraph(
"This is useful to place props suchs as fences, walls or anything that
needs to look organized without leaving gaps.")
documentation.add_warning(
"The transforms are placed starting from the begining of each curves.
If the curve is closed, there will be a gap at the end if the total
curve length isn't a multiple of the item length.")
p = documentation.add_parameter("Item length")
p.set_type("float")
p.set_description("How long is the item being placed")
p.set_cost(2)
p.add_warning(
"The smaller this value, the more transforms will be created.
Setting a slightly different length than the actual model length
allow for gaps between each transforms.")
p = documentation.add_parameter("Ignore slopes")
p.set_type("bool")
p.set_description(
"If enabled, all the curves will be projected to the local XZ plane
before creating the new transforms.")
# TODO: Use dichotomic search instead of fixed step length?
func _process_transforms(transforms, domain, seed) -> void:
var new_transforms: Array[Transform3D] = []
var curves: Array[Curve3D] = domain.get_edges()
for curve in curves:
if not ignore_slopes:
curve = curve.duplicate()
else:
curve = get_projected_curve(curve, domain.get_global_transform())
var length_squared = pow(item_length, 2)
var offset_max = curve.get_baked_length()
var offset = 0.0
var step = item_length / 20.0
while offset < offset_max:
var start := curve.sample_baked(offset)
var end: Vector3
var dist: float
offset += item_length * 0.9 # Saves a few iterations, the target
# point will never be closer than the item length, only further
while offset < offset_max:
offset += step
end = curve.sample_baked(offset)
dist = start.distance_squared_to(end)
if dist >= length_squared:
var t = Transform3D()
t.origin = start + ((end - start) / 2.0)
new_transforms.push_back(t.looking_at(end, Vector3.UP))
break
transforms.append(new_transforms)
transforms.shuffle(seed)
func get_projected_curve(curve: Curve3D, t: Transform3D) -> Curve3D:
var points = curve.tessellate()
var new_curve = Curve3D.new()
for p in points:
p.y = t.origin.y
new_curve.add_point(p)
return new_curve

View File

@@ -0,0 +1,79 @@
@tool
extends "base_modifier.gd"
const Util := preload("../common/util.gd")
# TODO :
# + change alignement parameters to something more usable and intuitive
# + Use the curve up vector, default to local Y+ when not available
@export var spacing := 1.0
@export var offset := 0.0
@export var align_to_path := false
@export var align_up_axis := Vector3.UP
var _min_spacing := 0.05
func _init() -> void:
display_name = "Create Along Edge (Even)"
category = "Create"
warning_ignore_no_transforms = true
warning_ignore_no_shape = false
can_restrict_height = false
global_reference_frame_available = true
local_reference_frame_available = true
individual_instances_reference_frame_available = false
use_edge_data = true
var p
documentation.add_paragraph(
"Evenly create transforms along the edges of the ScatterShapes")
p = documentation.add_parameter("Spacing")
p.set_type("float")
p.set_description("How much space between the transforms origin")
p.set_cost(3)
p.add_warning("The smaller the value, the denser the resulting transforms list.", 1)
p.add_warning(
"A value of 0 would result in infinite transforms, so it's capped
to 0.05 at least.")
func _process_transforms(transforms, domain, seed) -> void:
spacing = max(_min_spacing, spacing)
var gt_inverse: Transform3D = domain.get_global_transform().affine_inverse()
var new_transforms: Array[Transform3D] = []
var curves: Array[Curve3D] = domain.get_edges()
for curve in curves:
var length: float = curve.get_baked_length()
var count := int(round(length / spacing))
var stepped_length: float = count * spacing
for i in count:
var curve_offset = i * spacing + abs(offset)
while curve_offset > stepped_length: # Loop back to the curve start if offset is too large
curve_offset -= stepped_length
var data : Array = Util.get_position_and_normal_at(curve, curve_offset)
var pos: Vector3 = data[0]
var normal: Vector3 = data[1]
if domain.is_point_excluded(pos):
continue
var t := Transform3D()
t.origin = pos
if align_to_path:
t = t.looking_at(normal + pos, align_up_axis)
elif is_using_global_space():
t.basis = gt_inverse.basis
new_transforms.push_back(t)
transforms.append(new_transforms)
transforms.shuffle(seed)

View File

@@ -0,0 +1,69 @@
@tool
extends "base_modifier.gd"
@export var instance_count := 10
@export var align_to_path := false
@export var align_up_axis := Vector3.UP
var _rng: RandomNumberGenerator
func _init() -> void:
display_name = "Create Along Edge (Random)"
category = "Create"
warning_ignore_no_transforms = true
warning_ignore_no_shape = false
use_edge_data = true
global_reference_frame_available = true
local_reference_frame_available = true
individual_instances_reference_frame_available = false
func _process_transforms(transforms, domain, random_seed) -> void:
_rng = RandomNumberGenerator.new()
_rng.set_seed(random_seed)
var gt_inverse: Transform3D = domain.get_global_transform().affine_inverse()
var new_transforms: Array[Transform3D] = []
var curves: Array[Curve3D] = domain.get_edges()
var total_curve_length := 0.0
for curve in curves:
var length: float = curve.get_baked_length()
total_curve_length += length
for curve in curves:
var length: float = curve.get_baked_length()
var local_instance_count: int = round((length / total_curve_length) * instance_count)
for i in local_instance_count:
var data = get_pos_and_normal(curve, _rng.randf() * length)
var pos: Vector3 = data[0]
var normal: Vector3 = data[1]
var t := Transform3D()
t.origin = pos
if align_to_path:
t = t.looking_at(normal + pos, align_up_axis)
elif is_using_global_space():
t.basis = gt_inverse.basis
new_transforms.push_back(t)
transforms.append(new_transforms)
func get_pos_and_normal(curve: Curve3D, offset : float) -> Array:
var pos: Vector3 = curve.sample_baked(offset)
var normal := Vector3.ZERO
var pos1
if offset + curve.get_bake_interval() < curve.get_baked_length():
pos1 = curve.sample_baked(offset + curve.get_bake_interval())
normal = (pos1 - pos)
else:
pos1 = curve.sample_baked(offset - curve.get_bake_interval())
normal = (pos - pos1)
return [pos, normal]

View File

@@ -0,0 +1,97 @@
@tool
extends "base_modifier.gd"
@export var spacing := Vector3(2.0, 2.0, 2.0)
var _min_spacing := 0.05
func _init() -> void:
display_name = "Create Inside (Grid)"
category = "Create"
warning_ignore_no_transforms = true
warning_ignore_no_shape = false
can_restrict_height = true
restrict_height = true
global_reference_frame_available = true
local_reference_frame_available = true
individual_instances_reference_frame_available = false
documentation.add_paragraph(
"Place transforms along the edges of the ScatterShapes")
documentation.add_paragraph(
"When [b]Local Space[/b] is enabled, the grid is aligned with the
Scatter root node. Otherwise, the grid is aligned with the global
axes."
)
var p = documentation.add_parameter("Spacing")
p.set_type("vector3")
p.set_description(
"Defines the grid size along the 3 axes. A spacing of 1 means 1 unit
of space between each transform on this axis.")
p.set_cost(3)
p.add_warning(
"The smaller the value, the denser the resulting transforms list.
Use with care as the performance impact will go up quickly.", 1)
p.add_warning(
"A value of 0 would result in infinite transforms, so it's capped to 0.05
at least.")
func _process_transforms(transforms, domain, seed) -> void:
spacing.x = max(_min_spacing, spacing.x)
spacing.y = max(_min_spacing, spacing.y)
spacing.z = max(_min_spacing, spacing.z)
var gt: Transform3D = domain.get_local_transform()
var center: Vector3 = domain.bounds_local.center
var size: Vector3 = domain.bounds_local.size
var half_size := size * 0.5
var start_corner := center - half_size
var baseline: float = 0.0
var width := int(ceil(size.x / spacing.x))
var height := int(ceil(size.y / spacing.y))
var length := int(ceil(size.z / spacing.z))
if restrict_height:
height = 1
baseline = domain.bounds_local.max.y
else:
height = max(1, height) # Make sure height never gets below 1 or else nothing happens
var max_count: int = width * length * height
var new_transforms: Array[Transform3D] = []
new_transforms.resize(max_count)
var t: Transform3D
var pos: Vector3
var t_index := 0
for i in width * length:
for j in height:
t = Transform3D()
pos = Vector3.ZERO
pos.x = (i % width) * spacing.x
pos.y = (j * spacing.y) + baseline
pos.z = (i / width) * spacing.z
pos += start_corner
if is_using_global_space():
t.basis = gt.affine_inverse().basis
pos = t * pos
if domain.is_point_inside(pos):
t.origin = pos
new_transforms[t_index] = t
t_index += 1
if t_index != new_transforms.size():
new_transforms.resize(t_index)
transforms.append(new_transforms)
transforms.shuffle(seed)

View File

@@ -0,0 +1,230 @@
@tool
extends "base_modifier.gd"
# Poisson disc sampling based on Sebastian Lague implementation, modified to
# support both 2D and 3D space.
# Reference: https://www.youtube.com/watch?v=7WcmyxyFO7o
# TODO: This doesn't work if the valid space isn't one solid space
# (fails to fill the full domain if it's made of discrete, separate shapes)
const Bounds := preload("../common/bounds.gd")
@export var radius := 1.0
@export var samples_before_rejection := 15
var _rng: RandomNumberGenerator
var _squared_radius: float
var _domain
var _bounds: Bounds
var _gt: Transform3D
var _points: Array[Transform3D] # Stores the generated points
var _grid: Array[int] = [] # Flattened array
var _grid_size := Vector3i.ZERO
var _cell_size: float
var _cell_x: int
var _cell_y: int
var _cell_z: int
func _init() -> void:
display_name = "Create Inside (Poisson)"
category = "Create"
warning_ignore_no_transforms = true
warning_ignore_no_shape = false
can_restrict_height = true
can_override_seed = true
restrict_height = true
global_reference_frame_available = true
local_reference_frame_available = true
individual_instances_reference_frame_available = false
use_local_space_by_default()
documentation.add_paragraph(
"Place transforms without overlaps. Transforms are assumed to have a
spherical shape.")
var p := documentation.add_parameter("Radius")
p.set_type("float")
p.set_description("Transform size.")
p.add_warning(
"The larger the radius, the harder it will be to place the transform,
resulting in a faster early exit.
On the other hand, smaller radius means more room for more points,
meaning more transforms to generate so it will take longer to complete.")
p = documentation.add_parameter("Samples before rejection")
p.set_type("int")
p.set_description(
"The algorithm tries a point at random until it finds a valid one. This
parameter controls how many attempts before moving to the next
iteration. Lower values are faster but gives poor coverage. Higher
values generates better coverage but are slower.")
p.set_cost(2)
documentation.add_warning(
"This modifier uses a poisson disk sampling algorithm which can be
quite slow.")
func _process_transforms(transforms, domain, seed) -> void:
_rng = RandomNumberGenerator.new()
_rng.set_seed(seed)
_domain = domain
_bounds = _domain.bounds_local
_gt = domain.get_global_transform()
_points = []
_init_grid()
# Stores the possible starting points from where we run the sampling.
# This array will progressively be emptied as the algorithm progresses.
var spawn_points: Array[Transform3D]
spawn_points.push_back(_get_starting_point())
# Sampler main loop
while not spawn_points.is_empty():
# Pick a starting point at random from the existing list
var spawn_index: int = _rng.randi_range(0, spawn_points.size() - 1)
var spawn_center := spawn_points[spawn_index]
var tries := 0
var candidate_accepted := false
while tries < samples_before_rejection:
tries += 1
# Generate a random point in space, outside the radius of the spawn point
var dir: Vector3 = _generate_random_vector()
var candidate: Vector3 = spawn_center.origin + dir * _rng.randf_range(radius, radius * 2.0)
if _is_valid(candidate):
candidate_accepted = true
# Add new points to the lists
var t = Transform3D()
t.origin = candidate
if is_using_global_space():
t.basis = _gt.affine_inverse().basis
_points.push_back(t)
spawn_points.push_back(t)
var index: int
if restrict_height:
index = _cell_x + _cell_z * _grid_size.z
else:
index = _cell_x + (_grid_size.y * _cell_y) + (_grid_size.x * _grid_size.y * _cell_z)
if index < _grid.size():
_grid[index] = _points.size() - 1
break
# Failed to find a point after too many tries. The space around this
# spawn point is probably full, discard it.
if not candidate_accepted:
spawn_points.remove_at(spawn_index)
transforms.append(_points)
transforms.shuffle(seed)
func _init_grid() -> void:
_squared_radius = radius * radius
_cell_size = radius / sqrt(2)
_grid_size.x = ceil(_bounds.size.x / _cell_size)
_grid_size.y = ceil(_bounds.size.y / _cell_size)
_grid_size.z = ceil(_bounds.size.z / _cell_size)
_grid_size = _grid_size.clamp(Vector3.ONE, _grid_size)
_grid = []
if restrict_height:
_grid.resize(_grid_size.x * _grid_size.z)
else:
_grid.resize(_grid_size.x * _grid_size.y * _grid_size.z)
# Starting point must be inside the domain, or we run the risk to never generate
# any valid point later on
# TODO: Domain may have islands, so we should use multiple starting points
func _get_starting_point() -> Transform3D:
var point: Vector3 = _bounds.center
var tries := 0
while not _domain.is_point_inside(point) or tries > 200:
tries += 1
point.x = _rng.randf_range(_bounds.min.x, _bounds.max.x)
point.y = _rng.randf_range(_bounds.min.y, _bounds.max.y)
point.z = _rng.randf_range(_bounds.min.z, _bounds.max.z)
if restrict_height:
point.y = _bounds.center.y
var starting_point := Transform3D()
starting_point.origin = point
return starting_point
func _is_valid(candidate: Vector3) -> bool:
if not _domain.is_point_inside(candidate):
return false
# compute candidate current cell
var t_candidate = candidate - _bounds.min
_cell_x = floor(t_candidate.x / _cell_size)
_cell_y = floor(t_candidate.y / _cell_size)
_cell_z = floor(t_candidate.z / _cell_size)
# Search the surrounding cells for other points
var search_start_x: int = max(0, _cell_x - 2)
var search_end_x: int = min(_cell_x + 2, _grid_size.x - 1)
var search_start_y: int = max(0, _cell_y - 2)
var search_end_y: int = min(_cell_y + 2, _grid_size.y - 1)
var search_start_z: int = max(0, _cell_z - 2)
var search_end_z: int = min(_cell_z + 2, _grid_size.z - 1)
if restrict_height:
for x in range(search_start_x, search_end_x + 1):
for z in range(search_start_z, search_end_z + 1):
var point_index = _grid[x + z * _grid_size.z]
if _is_point_too_close(candidate, point_index):
return false
else:
for x in range(search_start_x, search_end_x + 1):
for y in range(search_start_y, search_end_y + 1):
for z in range(search_start_z, search_end_z + 1):
var point_index = _grid[x + (_grid_size.y * y) + (_grid_size.x * _grid_size.y * z)]
if _is_point_too_close(candidate, point_index):
return false
return true
func _is_point_too_close(candidate: Vector3, point_index: int) -> bool:
if point_index >= _points.size():
return false
var other_point := _points[point_index]
var squared_dist: float = candidate.distance_squared_to(other_point.origin)
return squared_dist < _squared_radius
func _generate_random_vector():
var angle = _rng.randf_range(0.0, TAU)
if restrict_height:
return Vector3(sin(angle), 0.0, cos(angle))
var costheta = _rng.randf_range(-1.0, 1.0)
var theta = acos(costheta)
var vector := Vector3.ZERO
vector.x = sin(theta) * cos(angle)
vector.y = sin(theta) * sin(angle)
vector.z = cos(theta)
return vector

View File

@@ -0,0 +1,84 @@
@tool
extends "base_modifier.gd"
@export var amount := 10
var _rng: RandomNumberGenerator
func _init() -> void:
display_name = "Create Inside (Random)"
category = "Create"
warning_ignore_no_transforms = true
warning_ignore_no_shape = false
can_override_seed = true
global_reference_frame_available = true
local_reference_frame_available = true
use_local_space_by_default()
documentation.add_paragraph(
"Randomly place new transforms inside the area defined by
the ScatterShape nodes.")
var p := documentation.add_parameter("Amount")
p.set_type("int")
p.set_description("How many transforms will be created.")
p.set_cost(2)
documentation.add_warning(
"In some cases, the amount of transforms created by this modifier
might be lower than the requested amount (but never higher). This may
happen if the provided ScatterShape has a huge bounding box but a tiny
valid space, like a curved and narrow path.")
# TODO:
# + Multithreading
# + Spatial partionning to discard areas outside the domain earlier
func _process_transforms(transforms, domain, random_seed) -> void:
_rng = RandomNumberGenerator.new()
_rng.set_seed(random_seed)
var gt: Transform3D = domain.get_global_transform()
var center: Vector3 = domain.bounds_local.center
var half_size: Vector3 = domain.bounds_local.size / 2.0
var height: float = domain.bounds_local.center.y
# Generate a random point in the bounding box. Store if it's inside the
# domain, or discard if invalid. Repeat until enough valid points are found.
var t: Transform3D
var pos: Vector3
var new_transforms: Array[Transform3D] = []
var max_retries = amount * 10 # TODO: expose this parameter?
var tries := 0
while new_transforms.size() != amount:
t = Transform3D()
pos = _random_vec3() * half_size + center
if restrict_height:
pos.y = height
if is_using_global_space():
t.basis = gt.affine_inverse().basis
if domain.is_point_inside(pos):
t.origin = pos
new_transforms.push_back(t)
continue
# Prevents an infinite loop
tries += 1
if tries > max_retries:
break
transforms.append(new_transforms)
func _random_vec3() -> Vector3:
var vec3 = Vector3.ZERO
vec3.x = _rng.randf_range(-1.0, 1.0)
vec3.y = _rng.randf_range(-1.0, 1.0)
vec3.z = _rng.randf_range(-1.0, 1.0)
return vec3

View File

@@ -0,0 +1,39 @@
@tool
extends "base_modifier.gd"
@export var target := Vector3.ZERO
@export var up := Vector3.UP
func _init() -> void:
display_name = "Look At"
category = "Edit"
can_restrict_height = false
global_reference_frame_available = true
local_reference_frame_available = true
individual_instances_reference_frame_available = true
use_local_space_by_default()
documentation.add_paragraph("Rotates every transform such that the forward axis (-Z) points towards the target position.")
documentation.add_parameter("Target").set_type("Vector3").set_description(
"Target position (X, Y, Z)")
documentation.add_parameter("Up").set_type("Vector3").set_description(
"Up axes (X, Y, Z)")
func _process_transforms(transforms, domain, _seed : int) -> void:
var st: Transform3D = domain.get_global_transform()
for i in transforms.size():
var transform: Transform3D = transforms.list[i]
var local_target := target
if is_using_global_space():
local_target = st.affine_inverse().basis * local_target
elif is_using_individual_instances_space():
local_target = transform.basis * local_target
transforms.list[i] = transform.looking_at(local_target, up)

View File

@@ -0,0 +1,63 @@
@tool
extends "base_modifier.gd"
@export_enum("Offset:0", "Multiply:1", "Override:2") var operation: int
@export var position := Vector3.ZERO
func _init() -> void:
display_name = "Edit Position"
category = "Offset"
can_restrict_height = false
global_reference_frame_available = true
local_reference_frame_available = true
individual_instances_reference_frame_available = true
use_individual_instances_space_by_default()
documentation.add_paragraph("Moves every transform the same way.")
var p := documentation.add_parameter("Position")
p.set_type("vector3")
p.set_description("How far each transforms are moved.")
func _process_transforms(transforms, domain, _seed) -> void:
var s_gt: Transform3D = domain.get_global_transform()
var s_gt_inverse: Transform3D = s_gt.affine_inverse()
var t: Transform3D
for i in transforms.list.size():
t = transforms.list[i]
var value: Vector3
if is_using_individual_instances_space():
value = t.basis * position
elif is_using_global_space():
value = s_gt_inverse.basis * position
else:
value = position
match operation:
0:
t.origin += value
1:
if is_using_local_space():
t.origin *= value
if is_using_global_space():
var global_pos = s_gt * t.origin
global_pos -= s_gt.origin
global_pos *= position
global_pos += s_gt.origin
t.origin = s_gt_inverse * global_pos
elif is_using_individual_instances_space():
pass # Multiply does nothing on this reference frame.
2:
t.origin = value
transforms.list[i] = t

View File

@@ -0,0 +1,100 @@
@tool
extends "base_modifier.gd"
@export_enum("Offset:0", "Multiply:1", "Override:2") var operation: int
@export var rotation := Vector3.ZERO
func _init() -> void:
display_name = "Edit Rotation"
category = "Offset"
can_restrict_height = false
global_reference_frame_available = true
local_reference_frame_available = true
individual_instances_reference_frame_available = true
use_individual_instances_space_by_default()
documentation.add_paragraph("Rotates every transform.")
documentation.add_parameter("Rotation").set_type("Vector3").set_description(
"Rotation angle (in degrees) along each axes (X, Y, Z)")
func _process_transforms(transforms, domain, _seed : int) -> void:
var rotation_rad := Vector3.ZERO
rotation_rad.x = deg_to_rad(rotation.x)
rotation_rad.y = deg_to_rad(rotation.y)
rotation_rad.z = deg_to_rad(rotation.z)
var s_gt: Transform3D = domain.get_global_transform()
var s_lt: Transform3D = domain.get_local_transform()
var s_gt_inverse := s_gt.affine_inverse()
var t: Transform3D
var basis: Basis
var axis_x: Vector3
var axis_y: Vector3
var axis_z: Vector3
var final_rotation: Vector3
if is_using_local_space():
axis_x = Vector3.RIGHT
axis_y = Vector3.UP
axis_z = Vector3.FORWARD
elif is_using_global_space():
axis_x = (s_gt_inverse.basis * Vector3.RIGHT).normalized()
axis_y = (s_gt_inverse.basis * Vector3.UP).normalized()
axis_z = (s_gt_inverse.basis * Vector3.FORWARD).normalized()
for i in transforms.size():
t = transforms.list[i]
basis = t.basis
match operation:
0: # Offset
final_rotation = rotation_rad
1: # Multiply
# TMP: Local and global space calculations are probably wrong
var current_rotation: Vector3
if is_using_individual_instances_space():
current_rotation = basis.get_euler()
elif is_using_local_space():
var local_t := t * s_lt
current_rotation = local_t.basis.get_euler()
else:
var global_t := t * s_gt
current_rotation = global_t.basis.get_euler()
final_rotation = (current_rotation * rotation) - current_rotation
2: # Override
# Creates a new basis with the original scale only
# Applies new rotation on top
if is_using_individual_instances_space():
basis = Basis().from_scale(t.basis.get_scale())
elif is_using_local_space():
basis = (s_gt_inverse * s_gt).basis
else:
var tmp_t = Transform3D(Basis.from_scale(t.basis.get_scale()), Vector3.ZERO)
basis = (s_gt_inverse * tmp_t).basis
final_rotation = rotation_rad
if is_using_individual_instances_space():
axis_x = basis.x.normalized()
axis_y = basis.y.normalized()
axis_z = basis.z.normalized()
basis = basis.rotated(axis_y, final_rotation.y)
basis = basis.rotated(axis_x, final_rotation.x)
basis = basis.rotated(axis_z, final_rotation.z)
transforms.list[i].basis = basis

View File

@@ -0,0 +1,94 @@
@tool
extends "base_modifier.gd"
@export_enum("Offset:0", "Multiply:1", "Override:2") var operation: int = 1
@export var scale := Vector3(1, 1, 1)
func _init() -> void:
display_name = "Edit Scale"
category = "Offset"
can_restrict_height = false
global_reference_frame_available = true
local_reference_frame_available = true
individual_instances_reference_frame_available = true
use_individual_instances_space_by_default()
documentation.add_paragraph("Scales every transform.")
var p := documentation.add_parameter("Scale")
p.set_type("Vector3")
p.set_description("How much to scale the transform along each axes (X, Y, Z)")
func _process_transforms(transforms, domain, _seed) -> void:
var s_gt: Transform3D = domain.get_global_transform()
var s_lt: Transform3D = domain.get_local_transform()
var s_gt_inverse := s_gt.affine_inverse()
var s_lt_inverse := s_lt.affine_inverse()
var basis: Basis
var t: Transform3D
var tmp_t: Transform3D
for i in transforms.size():
t = transforms.list[i]
basis = t.basis
match operation:
0: # Offset
if is_using_individual_instances_space():
var current_scale := basis.get_scale()
var s = (current_scale + scale) / current_scale
basis = t.scaled_local(s).basis
elif is_using_global_space():
# Convert to global space, scale, convert back to local space
tmp_t = s_gt * t
var current_scale: Vector3 = tmp_t.basis.get_scale()
tmp_t.basis = tmp_t.basis.scaled((current_scale + scale) / current_scale)
basis = (s_gt_inverse * tmp_t).basis
else:
var current_scale: Vector3 = basis.get_scale()
basis = basis.scaled((current_scale + scale) / current_scale)
1: # Multiply
if is_using_individual_instances_space():
basis = t.scaled_local(scale).basis
elif is_using_global_space():
# Convert to global space, scale, convert back to local space
tmp_t = s_gt * t
tmp_t = tmp_t.scaled(scale)
basis = (s_gt_inverse * tmp_t).basis
else:
basis = basis.scaled(scale)
2: # Override
if is_using_individual_instances_space():
var t_scale: Vector3 = basis.get_scale()
t_scale.x = (1.0 / t_scale.x) * scale.x
t_scale.y = (1.0 / t_scale.y) * scale.y
t_scale.z = (1.0 / t_scale.z) * scale.z
basis = t.scaled_local(t_scale).basis
elif is_using_global_space():
# Convert to global space, scale, convert back to local space
tmp_t = t * s_gt
var t_scale: Vector3 = tmp_t.basis.get_scale()
t_scale.x = (1.0 / t_scale.x) * scale.x
t_scale.y = (1.0 / t_scale.y) * scale.y
t_scale.z = (1.0 / t_scale.z) * scale.z
tmp_t.basis = tmp_t.basis.scaled(t_scale)
basis = (s_gt_inverse * tmp_t).basis
else:
var t_scale: Vector3 = basis.get_scale()
t_scale.x = (1.0 / t_scale.x) * scale.x
t_scale.y = (1.0 / t_scale.y) * scale.y
t_scale.z = (1.0 / t_scale.z) * scale.z
basis = basis.scaled(t_scale)
transforms.list[i].basis = basis

View File

@@ -0,0 +1,83 @@
@tool
extends "base_modifier.gd"
@export var position := Vector3.ZERO
@export var rotation := Vector3(0.0, 0.0, 0.0)
@export var scale := Vector3.ONE
func _init() -> void:
display_name = "Edit Transform"
category = "Offset"
can_restrict_height = false
global_reference_frame_available = true
local_reference_frame_available = true
individual_instances_reference_frame_available = true
use_local_space_by_default()
deprecated = true
deprecation_message = "Use a combination of 'Edit Position', 'Edit Rotation' and 'Edit Scale' instead."
documentation.add_paragraph(
"Offsets position, rotation and scale in a single modifier. Every
transforms generated before will see the same transformation applied.")
var p := documentation.add_parameter("Position")
p.set_type("Vector3")
p.set_description("How far each transforms are moved.")
p = documentation.add_parameter("Rotation")
p.set_type("Vector3")
p.set_description("Rotation angle (in degrees) along each axes (X, Y, Z)")
p = documentation.add_parameter("Scale")
p.set_type("Vector3")
p.set_description("How much to scale the transform along each axes (X, Y, Z)")
func _process_transforms(transforms, domain, _seed) -> void:
var t: Transform3D
var local_t: Transform3D
var basis: Basis
var axis_x := Vector3.RIGHT
var axis_y := Vector3.UP
var axis_z := Vector3.DOWN
var final_scale := scale
var final_position := position
var st: Transform3D = domain.get_global_transform()
if is_using_local_space():
axis_x = st.basis.x
axis_y = st.basis.y
axis_z = st.basis.z
final_scale = scale.rotated(Vector3.RIGHT, st.basis.get_euler().x)
final_position = st.basis * position
for i in transforms.size():
t = transforms.list[i]
basis = t.basis
if is_using_individual_instances_space():
axis_x = basis.x
axis_y = basis.y
axis_z = basis.z
basis.x *= scale.x
basis.y *= scale.y
basis.z *= scale.z
final_position = t.basis * position
elif is_using_local_space():
local_t = t * st
local_t.basis = local_t.basis.scaled(final_scale)
basis = (st * local_t).basis
else:
basis = basis.scaled(final_scale)
basis = basis.rotated(axis_x, deg_to_rad(rotation.x))
basis = basis.rotated(axis_y, deg_to_rad(rotation.y))
basis = basis.rotated(axis_z, deg_to_rad(rotation.z))
t.basis = basis
t.origin += final_position
transforms.list[i] = t

View File

@@ -0,0 +1,216 @@
@tool
extends "base_modifier.gd"
signal projection_completed
const ProtonScatterPhysicsHelper := preload("res://addons/proton_scatter/src/common/physics_helper.gd")
@export var ray_direction := Vector3.DOWN
@export var ray_length := 10.0
@export var ray_offset := 1.0
@export var remove_points_on_miss := true
@export var align_with_collision_normal := false
@export_range(0.0, 90.0) var max_slope = 90.0
@export_flags_3d_physics var collision_mask = 1
@export_flags_3d_physics var exclude_mask = 0
func _init() -> void:
display_name = "Project On Colliders"
category = "Edit"
can_restrict_height = false
global_reference_frame_available = true
local_reference_frame_available = true
individual_instances_reference_frame_available = true
use_global_space_by_default()
documentation.add_paragraph(
"Moves each transforms along the ray direction until they hit a collider.
This is useful to avoid floating objects on uneven terrain for example.")
documentation.add_warning(
"This modifier only works when physics bodies are around. It will ignore
simple MeshInstances nodes.")
var p := documentation.add_parameter("Ray direction")
p.set_type("Vector3")
p.set_description(
"In which direction we look for a collider. This default to the DOWN
direction by default (look at the ground).")
p.add_warning(
"This is relative to the transform is local space is enabled, or aligned
with the global axis if local space is disabled.")
p = documentation.add_parameter("Ray length")
p.set_type("float")
p.set_description("How far we look for other physics objects.")
p.set_cost(2)
p = documentation.add_parameter("Ray offset")
p.set_type("Vector3")
p.set_description(
"Moves back the raycast origin point along the ray direction. This is
useful if the initial transform is slightly below the ground, which would
make the raycast miss the collider (since it would start inside).")
p = documentation.add_parameter("Remove points on miss")
p.set_type("bool")
p.set_description(
"When enabled, if the raycast didn't collide with anything, or collided
with a surface above the max slope setting, the transform is removed
from the list.
This is useful to avoid floating objects that are too far from the rest
of the scene's geometry.")
p = documentation.add_parameter("Align with collision normal")
p.set_type("bool")
p.set_description(
"Rotate the transform to align it with the collision normal in case
the ray cast hit a collider.")
p = documentation.add_parameter("Max slope")
p.set_type("float")
p.set_description(
"Angle (in degrees) after which the hit is considered invalid.
When a ray cast hit, the normal of the ray is compared against the
normal of the hit. If you set the slope to 0°, the ray and the hit
normal would have to be perfectly aligned to be valid. On the other
hand, setting the maximum slope to 90° treats every collisions as
valid regardless of their normals.")
p = documentation.add_parameter("Mask")
p.set_description(
"Only collide with colliders on these layers. Disabled layers will
be ignored. It's useful to ignore players or npcs that might be on the
scene when you're editing it.")
p = documentation.add_parameter("Exclude Mask")
p.set_description(
"Tests if the snapping would collide with the selected layers.
If it collides, the point will be excluded from the list.")
func _process_transforms(transforms, domain, _seed) -> void:
if transforms.is_empty():
return
# Create all the physics ray queries
var gt: Transform3D = domain.get_global_transform()
var gt_inverse := gt.affine_inverse()
var queries: Array[PhysicsRayQueryParameters3D] = []
var exclude_queries: Array[PhysicsRayQueryParameters3D] = []
for t in transforms.list:
var start = gt * t.origin
var end = start
var dir = ray_direction.normalized()
if is_using_individual_instances_space():
dir = t.basis * dir
elif is_using_local_space():
dir = gt.basis * dir
start -= ray_offset * dir
end += ray_length * dir
var ray_query := PhysicsRayQueryParameters3D.new()
ray_query.from = start
ray_query.to = end
ray_query.collision_mask = collision_mask
queries.push_back(ray_query)
var exclude_query := PhysicsRayQueryParameters3D.new()
exclude_query.from = start
exclude_query.to = end
exclude_query.collision_mask = exclude_mask
exclude_queries.push_back(exclude_query)
# Run the queries in the physics helper since we can't access the PhysicsServer
# from outside the _physics_process while also being in a separate thread.
var physics_helper: ProtonScatterPhysicsHelper = domain.get_root().get_physics_helper()
var ray_hits := await physics_helper.execute(queries)
if ray_hits.is_empty():
return
# Create queries from the hit points
var index := -1
for ray_hit in ray_hits:
index += 1
var hit : Dictionary = ray_hit
if hit.is_empty():
exclude_queries[index].collision_mask = 0 # this point is empty anyway, we dont care
continue
exclude_queries[index].to = hit.position # only cast up to hit point for correct ordering
var exclude_hits : Array[Dictionary] = []
if exclude_mask != 0: # Only cast the rays if it makes any sense
exclude_hits = await physics_helper.execute(exclude_queries)
# Apply the results
index = 0
var d: float
var t: Transform3D
var remapped_max_slope = remap(max_slope, 0.0, 90.0, 0.0, 1.0)
var is_point_valid := false
exclude_hits.reverse() # makes it possible to use pop_back which is much faster
var new_transforms_array : Array[Transform3D] = []
for hit in ray_hits:
is_point_valid = true
if hit.is_empty():
is_point_valid = false
else:
d = abs(Vector3.UP.dot(hit.normal))
is_point_valid = d >= (1.0 - remapped_max_slope)
var exclude_hit = exclude_hits.pop_back()
if exclude_hit != null:
if not exclude_hit.is_empty():
is_point_valid = false
t = transforms.list[index]
if is_point_valid:
if align_with_collision_normal:
t = _align_with(t, gt_inverse.basis * hit.normal)
t.origin = gt_inverse * hit.position
new_transforms_array.push_back(t)
elif not remove_points_on_miss:
new_transforms_array.push_back(t)
index += 1
# All done, store the transforms in the original array
transforms.list.clear()
transforms.list.append_array(new_transforms_array) # this avoids memory leak
if transforms.is_empty():
warning += """Every points have been removed. Possible reasons include: \n
+ No collider is close enough to the shapes.
+ Ray length is too short.
+ Ray direction is incorrect.
+ Collision mask is not set properly.
+ Max slope is too low.
"""
func _align_with(t: Transform3D, normal: Vector3) -> Transform3D:
var n1 = t.basis.y.normalized()
var n2 = normal.normalized()
var cosa = n1.dot(n2)
var alpha = acos(cosa)
var axis = n1.cross(n2)
if axis == Vector3.ZERO:
return t
return t.rotated(axis.normalized(), alpha)

View File

@@ -0,0 +1,75 @@
@tool
extends "base_modifier.gd"
const ProtonScatter := preload("../scatter.gd")
const ModifierStack := preload("../stack/modifier_stack.gd")
@export_node_path var scatter_node: NodePath
@export var auto_rebuild := true:
set(val):
auto_rebuild = val
if not is_instance_valid(_source_node) or not _source_node is ProtonScatter:
return
if auto_rebuild: # Connect signal if not already connected
if not _source_node.build_completed.is_connected(_on_source_changed):
_source_node.build_completed.connect(_on_source_changed)
# Auto rebuild disabled, disconnect signal if connected
elif _source_node.build_completed.is_connected(_on_source_changed):
_source_node.build_completed.disconnect(_on_source_changed)
var _source_node: ProtonScatter:
set(val):
# Disconnect signals from previous scatter node if any
if is_instance_valid(_source_node) and _source_node is ProtonScatter:
if _source_node.build_completed.is_connected(_on_source_changed):
_source_node.build_completed.disconnect(_on_source_changed)
# Replace reference and retrigger the auto_rebuild setter
_source_node = val
auto_rebuild = auto_rebuild
func _init() -> void:
display_name = "Proxy"
category = "Misc"
can_restrict_height = false
can_override_seed = false
global_reference_frame_available = false
local_reference_frame_available = false
individual_instances_reference_frame_available = false
warning_ignore_no_transforms = true
documentation.add_paragraph("Copy a modifier stack from another ProtonScatter node in the scene.")
documentation.add_paragraph(
"Useful when you need multiple Scatter nodes sharing the same rules, without having to
replicate their modifiers and settings in each."
)
documentation.add_paragraph(
"Unlike presets which are full independent copies, this method is more similar to a linked
copy. Changes on the original modifier stack will be accounted for in here."
)
var p = documentation.add_parameter("Scatter node")
p.set_type("NodePath")
p.set_description("The Scatter node to use as a reference.")
func _process_transforms(transforms, domain, _seed) -> void:
_source_node = domain.get_root().get_node_or_null(scatter_node)
if not _source_node or not _source_node is ProtonScatter:
warning += "You need to select a valid ProtonScatter node."
return
if _source_node.modifier_stack:
var stack: ModifierStack = _source_node.modifier_stack.get_copy()
var results = await stack.start_update(domain.get_root(), domain)
transforms.append(results.list)
func _on_source_changed() -> void:
modifier_changed.emit()

View File

@@ -0,0 +1,88 @@
@tool
extends "base_modifier.gd"
@export var rotation := Vector3(360.0, 360.0, 360.0)
@export var snap_angle := Vector3.ZERO
var _rng: RandomNumberGenerator
func _init() -> void:
display_name = "Randomize Rotation"
category = "Edit"
can_override_seed = true
can_restrict_height = false
global_reference_frame_available = true
local_reference_frame_available = true
individual_instances_reference_frame_available = true
use_individual_instances_space_by_default()
documentation.add_paragraph("Randomly rotate every transforms individually.")
var p := documentation.add_parameter("Rotation")
p.set_type("Vector3")
p.set_description("Rotation angle (in degrees) along each axes (X, Y, Z)")
p = documentation.add_parameter("Snap angle")
p.set_type("Vector3")
p.set_description(
"When set to any value above 0, the rotation will be done by increments
of the snap angle.")
p.add_warning(
"Example: When Snap Angle is set to 90, the possible random rotation
offsets around an axis will be among [0, 90, 180, 360].")
func _process_transforms(transforms, domain, random_seed) -> void:
_rng = RandomNumberGenerator.new()
_rng.set_seed(random_seed)
var t: Transform3D
var b: Basis
var gt: Transform3D = domain.get_global_transform()
var gb: Basis = gt.basis
var axis_x: Vector3 = Vector3.RIGHT
var axis_y: Vector3 = Vector3.UP
var axis_z: Vector3 = Vector3.FORWARD
if is_using_local_space():
axis_x = (Vector3.RIGHT * gb).normalized()
axis_y = (Vector3.UP * gb).normalized()
axis_z = (Vector3.FORWARD * gb).normalized()
for i in transforms.list.size():
t = transforms.list[i]
b = t.basis
if is_using_individual_instances_space():
axis_x = t.basis.x.normalized()
axis_y = t.basis.y.normalized()
axis_z = t.basis.z.normalized()
b = b.rotated(axis_x, _random_angle(rotation.x, snap_angle.x))
b = b.rotated(axis_y, _random_angle(rotation.y, snap_angle.y))
b = b.rotated(axis_z, _random_angle(rotation.z, snap_angle.z))
t.basis = b
transforms.list[i] = t
func _random_vec3() -> Vector3:
var vec3 = Vector3.ZERO
vec3.x = _rng.randf_range(-1.0, 1.0)
vec3.y = _rng.randf_range(-1.0, 1.0)
vec3.z = _rng.randf_range(-1.0, 1.0)
return vec3
func _random_angle(rot: float, snap: float) -> float:
return deg_to_rad(snapped(_rng.randf_range(-1.0, 1.0) * rot, snap))
func _clamp_vector(vec3, vmin, vmax) -> Vector3:
vec3.x = clamp(vec3.x, vmin.x, vmax.x)
vec3.y = clamp(vec3.y, vmin.y, vmax.y)
vec3.z = clamp(vec3.z, vmin.z, vmax.z)
return vec3

View File

@@ -0,0 +1,106 @@
@tool
extends "base_modifier.gd"
@export var position := Vector3.ZERO
@export var rotation := Vector3.ZERO
@export var scale := Vector3.ZERO
var _rng: RandomNumberGenerator
func _init() -> void:
display_name = "Randomize Transforms"
category = "Edit"
can_override_seed = true
can_restrict_height = false
global_reference_frame_available = true
local_reference_frame_available = true
individual_instances_reference_frame_available = true
use_individual_instances_space_by_default()
func _process_transforms(transforms, domain, random_seed) -> void:
_rng = RandomNumberGenerator.new()
_rng.set_seed(random_seed)
var t: Transform3D
var global_t: Transform3D
var basis: Basis
var random_scale: Vector3
var random_position: Vector3
var s_gt: Transform3D = domain.get_global_transform()
var s_gt_inverse := s_gt.affine_inverse()
# Global rotation axis
var axis_x := Vector3.RIGHT
var axis_y := Vector3.UP
var axis_z := Vector3.DOWN
if is_using_global_space():
axis_x = (s_gt_inverse.basis * Vector3.RIGHT).normalized()
axis_y = (s_gt_inverse.basis * Vector3.UP).normalized()
axis_z = (s_gt_inverse.basis * Vector3.FORWARD).normalized()
for i in transforms.size():
t = transforms.list[i]
basis = t.basis
# Apply rotation
if is_using_individual_instances_space():
axis_x = basis.x.normalized()
axis_y = basis.y.normalized()
axis_z = basis.z.normalized()
basis = basis.rotated(axis_x, deg_to_rad(_random_float() * rotation.x))
basis = basis.rotated(axis_y, deg_to_rad(_random_float() * rotation.y))
basis = basis.rotated(axis_z, deg_to_rad(_random_float() * rotation.z))
# Apply scale
random_scale = Vector3.ONE + (_rng.randf() * scale)
if is_using_individual_instances_space():
basis.x *= random_scale.x
basis.y *= random_scale.y
basis.z *= random_scale.z
elif is_using_global_space():
global_t = s_gt * Transform3D(basis, Vector3.ZERO)
global_t = global_t.scaled(random_scale)
basis = (s_gt_inverse * global_t).basis
else:
basis = basis.scaled(random_scale)
# Apply position
random_position = _random_vec3() * position
if is_using_individual_instances_space():
random_position = t.basis * random_position
elif is_using_global_space():
random_position = s_gt_inverse.basis * random_position
t.origin += random_position
t.basis = basis
transforms.list[i] = t
func _random_vec3() -> Vector3:
var vec3 = Vector3.ZERO
vec3.x = _rng.randf_range(-1.0, 1.0)
vec3.y = _rng.randf_range(-1.0, 1.0)
vec3.z = _rng.randf_range(-1.0, 1.0)
return vec3
func _random_float() -> float:
return _rng.randf_range(-1.0, 1.0)
func _clamp_vector(vec3, vmin, vmax) -> Vector3:
vec3.x = clamp(vec3.x, vmin.x, vmax.x)
vec3.y = clamp(vec3.y, vmin.y, vmax.y)
vec3.z = clamp(vec3.z, vmin.z, vmax.z)
return vec3

View File

@@ -0,0 +1,191 @@
@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

View File

@@ -0,0 +1,44 @@
@tool
extends "base_modifier.gd"
@export var negative_shapes_only := false
func _init() -> void:
display_name = "Remove Outside"
category = "Remove"
can_restrict_height = false
global_reference_frame_available = false
local_reference_frame_available = false
individual_instances_reference_frame_available = false
documentation.add_paragraph(
"Remove all transforms falling outside a ScatterShape node, or inside
a shape set to 'Negative' mode.")
var p := documentation.add_parameter("Negative Shapes Only")
p.set_type("bool")
p.set_description(
"Only remove transforms falling inside the negative shapes (shown
in red). Transforms outside any shapes will still remain.")
func _process_transforms(transforms, domain, seed) -> void:
var i := 0
var point: Vector3
var to_remove := false
while i < transforms.size():
point = transforms.list[i].origin
if negative_shapes_only:
to_remove = domain.is_point_excluded(point)
else:
to_remove = not domain.is_point_inside(point)
if to_remove:
transforms.list.remove_at(i)
continue
i += 1

View File

@@ -0,0 +1,55 @@
@tool
extends "base_modifier.gd"
# Adds a single object with the given transform
@export var offset := Vector3.ZERO
@export var rotation := Vector3.ZERO
@export var scale := Vector3.ONE
func _init() -> void:
display_name = "Add Single Item"
category = "Create"
warning_ignore_no_shape = true
warning_ignore_no_transforms = true
can_restrict_height = false
global_reference_frame_available = true
local_reference_frame_available = true
individual_instances_reference_frame_available = false
use_local_space_by_default()
func _process_transforms(transforms, domain, _seed) -> void:
var gt: Transform3D = domain.get_global_transform()
var gt_inverse: Transform3D = gt.affine_inverse()
var t_origin := offset
var basis := Basis()
var x_axis = Vector3.RIGHT
var y_axis = Vector3.UP
var z_axis = Vector3.FORWARD
if is_using_global_space():
t_origin = gt_inverse.basis * t_origin
x_axis = gt_inverse.basis * x_axis
y_axis = gt_inverse.basis * y_axis
z_axis = gt_inverse.basis * z_axis
basis = gt_inverse.basis
basis = basis.rotated(x_axis, deg_to_rad(rotation.x))
basis = basis.rotated(y_axis, deg_to_rad(rotation.y))
basis = basis.rotated(z_axis, deg_to_rad(rotation.z))
var transform := Transform3D(basis, Vector3.ZERO)
if is_using_global_space():
var global_t: Transform3D = gt * transform
global_t.basis = global_t.basis.scaled(scale)
transform = gt_inverse * global_t
else:
transform = transform.scaled_local(scale)
transform.origin = t_origin
transforms.list.push_back(transform)

View File

@@ -0,0 +1,100 @@
@tool
extends "base_modifier.gd"
# TODO: This modifier has the same shortcomings as offset_rotation, but in every reference frame.
@export var position_step := Vector3.ZERO
@export var rotation_step := Vector3.ZERO
@export var scale_step := Vector3.ZERO
func _init() -> void:
display_name = "Snap Transforms"
category = "Edit"
can_restrict_height = false
global_reference_frame_available = true
local_reference_frame_available = true
individual_instances_reference_frame_available = true
use_individual_instances_space_by_default()
documentation.add_paragraph("Snap the individual transforms components.")
documentation.add_paragraph("Values of 0 do not affect the transforms.")
var p := documentation.add_parameter("Position")
p.set_type("Vector3")
p.set_description("Defines the grid size used to snap the transform position.")
p = documentation.add_parameter("Rotation")
p.set_type("Vector3")
p.set_description(
"When set to any value above 0, the rotation will be set to the nearest
multiple of that angle.")
p.add_warning(
"Example: If rotation is set to (0, 90.0, 0), the rotation around the Y
axis will be snapped to the closed value among [0, 90, 180, 360].")
p = documentation.add_parameter("Scale")
p.set_type("Vector3")
p.set_description("Snap the current scale to the nearest multiple.")
func _process_transforms(transforms, domain, _seed) -> void:
var s_gt: Transform3D = domain.get_global_transform()
var s_lt: Transform3D = domain.get_local_transform()
var s_gt_inverse := s_gt.affine_inverse()
var axis_x := Vector3.RIGHT
var axis_y := Vector3.UP
var axis_z := Vector3.FORWARD
if is_using_global_space():
axis_x = (s_gt_inverse.basis * Vector3.RIGHT).normalized()
axis_y = (s_gt_inverse.basis * Vector3.UP).normalized()
axis_z = (s_gt_inverse.basis * Vector3.FORWARD).normalized()
var rotation_step_rad := Vector3.ZERO
rotation_step_rad.x = deg_to_rad(rotation_step.x)
rotation_step_rad.y = deg_to_rad(rotation_step.y)
rotation_step_rad.z = deg_to_rad(rotation_step.z)
for i in transforms.size():
var t: Transform3D = transforms.list[i]
var b := Basis()
var current_rotation: Vector3
if is_using_individual_instances_space():
axis_x = t.basis.x.normalized()
axis_y = t.basis.y.normalized()
axis_z = t.basis.z.normalized()
current_rotation = t.basis.get_euler()
t.origin = snapped(t.origin, position_step)
elif is_using_local_space():
var local_t := s_lt * t
current_rotation = local_t.basis.get_euler()
t.origin = snapped(t.origin, position_step)
else:
b = (s_gt_inverse * Transform3D()).basis
var global_t := s_gt * t
current_rotation = global_t.basis.get_euler()
t.origin = s_gt_inverse * snapped(global_t.origin, position_step)
b = b.rotated(axis_x, snapped(current_rotation.x, rotation_step_rad.x))
b = b.rotated(axis_y, snapped(current_rotation.y, rotation_step_rad.y))
b = b.rotated(axis_z, snapped(current_rotation.z, rotation_step_rad.z))
# Snap scale
var current_scale := t.basis.get_scale()
var snapped_scale: Vector3 = snapped(current_scale, scale_step)
t.basis = b
transforms.list[i] = t.scaled_local(snapped_scale)
func _clamp_vector(vec3, vmin, vmax) -> Vector3:
vec3.x = clamp(vec3.x, vmin.x, vmax.x)
vec3.y = clamp(vec3.y, vmin.y, vmax.y)
vec3.z = clamp(vec3.z, vmin.z, vmax.z)
return vec3