Added footsteps, new tree, various other tweaks
This commit is contained in:
182
addons/proton_scatter/src/modifiers/array.gd
Normal file
182
addons/proton_scatter/src/modifiers/array.gd
Normal 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)
|
||||
126
addons/proton_scatter/src/modifiers/base_modifier.gd
Normal file
126
addons/proton_scatter/src/modifiers/base_modifier.gd
Normal 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
|
||||
131
addons/proton_scatter/src/modifiers/clusterize.gd
Normal file
131
addons/proton_scatter/src/modifiers/clusterize.gd
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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]
|
||||
97
addons/proton_scatter/src/modifiers/create_inside_grid.gd
Normal file
97
addons/proton_scatter/src/modifiers/create_inside_grid.gd
Normal 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)
|
||||
230
addons/proton_scatter/src/modifiers/create_inside_poisson.gd
Normal file
230
addons/proton_scatter/src/modifiers/create_inside_poisson.gd
Normal 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
|
||||
84
addons/proton_scatter/src/modifiers/create_inside_random.gd
Normal file
84
addons/proton_scatter/src/modifiers/create_inside_random.gd
Normal 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
|
||||
39
addons/proton_scatter/src/modifiers/look_at.gd
Normal file
39
addons/proton_scatter/src/modifiers/look_at.gd
Normal 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)
|
||||
63
addons/proton_scatter/src/modifiers/offset_position.gd
Normal file
63
addons/proton_scatter/src/modifiers/offset_position.gd
Normal 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
|
||||
100
addons/proton_scatter/src/modifiers/offset_rotation.gd
Normal file
100
addons/proton_scatter/src/modifiers/offset_rotation.gd
Normal 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
|
||||
94
addons/proton_scatter/src/modifiers/offset_scale.gd
Normal file
94
addons/proton_scatter/src/modifiers/offset_scale.gd
Normal 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
|
||||
83
addons/proton_scatter/src/modifiers/offset_transform.gd
Normal file
83
addons/proton_scatter/src/modifiers/offset_transform.gd
Normal 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
|
||||
216
addons/proton_scatter/src/modifiers/project_on_geometry.gd
Normal file
216
addons/proton_scatter/src/modifiers/project_on_geometry.gd
Normal 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)
|
||||
75
addons/proton_scatter/src/modifiers/proxy.gd
Normal file
75
addons/proton_scatter/src/modifiers/proxy.gd
Normal 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()
|
||||
88
addons/proton_scatter/src/modifiers/randomize_rotation.gd
Normal file
88
addons/proton_scatter/src/modifiers/randomize_rotation.gd
Normal 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
|
||||
106
addons/proton_scatter/src/modifiers/randomize_transforms.gd
Normal file
106
addons/proton_scatter/src/modifiers/randomize_transforms.gd
Normal 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
|
||||
191
addons/proton_scatter/src/modifiers/relax.gd
Normal file
191
addons/proton_scatter/src/modifiers/relax.gd
Normal 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
|
||||
44
addons/proton_scatter/src/modifiers/remove_outside_shapes.gd
Normal file
44
addons/proton_scatter/src/modifiers/remove_outside_shapes.gd
Normal 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
|
||||
55
addons/proton_scatter/src/modifiers/single_item.gd
Normal file
55
addons/proton_scatter/src/modifiers/single_item.gd
Normal 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)
|
||||
100
addons/proton_scatter/src/modifiers/snap_transforms.gd
Normal file
100
addons/proton_scatter/src/modifiers/snap_transforms.gd
Normal 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
|
||||
Reference in New Issue
Block a user