Closed spiderbyte87 closed 1 year ago
I'll check if I can find a way to solve this from the add-on directly, scatter lacks a lot of features when it comes to making changes at runtime.
Right now, the only solution is to access the underlying nodes (when using 'Create copies') or the MultimeshInstance3D nodes (when using instancing) and edit them directly, which is not ideal.
I managed to write my own scatter script using a spatial hashing algorithm that removes instances instantly. Here's a vid: https://youtu.be/BZCUeiyF-DU
Here's the script (it's a little messy but it works):
extends MultiMeshInstance3D
@export var heightmap_texture: ViewportTexture
@export var heightmap_scale_factor: float = 50.0
@export var clustermap_texture: ViewportTexture
@export var density: int = 200
@export var max_instances: int = 1000
@export var terrain_size: float = 320.0
@export var min_height: float = 5.0 # minimum height where trees can be placed
@export var max_height: float = 45.0 # maximum height where trees can be placed
@export var max_slope: float = 0.3 # maximum allowable gradient magnitude for tree placement
@export var min_scale: Vector3 = Vector3(0.8, 0.8, 0.8)
@export var max_scale: Vector3 = Vector3(1.2, 1.2, 1.2)
@export var instance_scale_factor: float = 1.0
@export var cell_size: float = 10.0 # Adjust as needed
var spatial_hash = {}
var current_instance_count: int = 0
func _ready():
Globals.building_just_placed.connect(clear_instances)
await RenderingServer.frame_post_draw
# Populate the MultiMesh using heightmap
var heightmap_image = heightmap_texture.get_image()
populate_multimesh_from_heightmap(heightmap_image)
func _hash_position(pos: Vector3) -> String:
# Convert world position to cell coordinates
var cell_x = int(floor(pos.x / cell_size))
var cell_y = int(floor(pos.y / cell_size))
var cell_z = int(floor(pos.z / cell_size))
return "%d,%d,%d" % [cell_x, cell_y, cell_z]
func insert_into_spatial_hash(pos: Vector3, instance_id: int):
var hash_key = _hash_position(pos)
if spatial_hash.has(hash_key):
spatial_hash[hash_key].append({"pos": pos, "id": instance_id})
else:
spatial_hash[hash_key] = [{"pos": pos, "id": instance_id}]
func clear_instances(nearby, clear_radius):
var nearby_instances: Array = get_nearby_instances(nearby, clear_radius)
hideInstances(nearby_instances)
func hideInstances(instances: Array):
for instance in instances:
var instance_id = instance["id"]
var transform = multimesh.get_instance_transform(instance_id)
transform.basis = transform.basis.scaled(Vector3(0, 0, 0))
multimesh.set_instance_transform(instance_id, transform)
func get_nearby_instances(pos: Vector3, radius: float) -> Array:
var instances_within_radius = []
# Determine how many cells the radius might span in each dimension
var cells_in_radius = int(ceil(radius / cell_size))
# Compute the central cell of the provided position
var central_cell = _hash_position(pos).split(",")
var central_cell_x = int(central_cell[0])
var central_cell_y = int(central_cell[1])
var central_cell_z = int(central_cell[2])
# Loop through all potential cells that might intersect the radius
for x in range(central_cell_x - cells_in_radius, central_cell_x + cells_in_radius + 1):
for y in range(central_cell_y - cells_in_radius, central_cell_y + cells_in_radius + 1):
for z in range(central_cell_z - cells_in_radius, central_cell_z + cells_in_radius + 1):
var hash_key = "%d,%d,%d" % [x, y, z]
if spatial_hash.has(hash_key):
for instance_data in spatial_hash[hash_key]:
# Check if the instance is within the radius
if instance_data["pos"].distance_to(pos) <= radius:
instances_within_radius.append(instance_data) # Note: Appending the entire dictionary here
return instances_within_radius
func get_slope(heightmap_image, x, y):
# Define the Sobel kernels for x and y directions
var sobel_x = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]
var sobel_y = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]]
var dx = 0.0
var dy = 0.0
# Loop through the 3x3 neighborhood around the pixel
for i in range(-1, 2):
for j in range(-1, 2):
# Ensure we're within the bounds of the image
var nx = clamp(x + i, 0, heightmap_image.get_width() - 1)
var ny = clamp(y + j, 0, heightmap_image.get_height() - 1)
var height = heightmap_image.get_pixel(nx, ny).r * heightmap_scale_factor
dx += sobel_x[i + 1][j + 1] * height
dy += sobel_y[i + 1][j + 1] * height
# Compute the gradient magnitude
return sqrt(dx*dx + dy*dy)
func populate_multimesh_from_heightmap(heightmap_image):
# Make sure to load the clustermap image as well
var clustermap_image = clustermap_texture.get_image()
var width = heightmap_image.get_width()
var height = heightmap_image.get_height()
var scale_factor_x = terrain_size / width
var scale_factor_y = terrain_size / height
var half_terrain_size = terrain_size * 0.5
while current_instance_count < max_instances:
var x = randi() % width
var y = randi() % height
var cluster_value = clustermap_image.get_pixel(x, y).r
# If cluster value is close to 0, skip this iteration
if cluster_value < 0.3:
continue
var height_value = heightmap_image.get_pixel(x, y).r * heightmap_scale_factor
var slope = get_slope(heightmap_image, x, y)
# Adjust density based on cluster value
if randf() < density * cluster_value and height_value > min_height and height_value < max_height and slope < max_slope:
var world_pos_x = (x * scale_factor_x) - half_terrain_size
var world_pos_y = (y * scale_factor_y) - half_terrain_size
var transform = Transform3D.IDENTITY
# Apply random rotation around Y-axis
var rotation_angle = randf() * PI * 2.0 # A full circle in radians
transform = transform.rotated(Vector3(0, 1, 0), rotation_angle)
# Apply random scaling
var scale_factor = Vector3(
randf_range(min_scale.x * instance_scale_factor, max_scale.x * instance_scale_factor),
randf_range(min_scale.y * instance_scale_factor, max_scale.y * instance_scale_factor),
randf_range(min_scale.z * instance_scale_factor, max_scale.z * instance_scale_factor)
)
transform.basis = transform.basis.scaled(scale_factor)
# Apply translation last
transform.origin = Vector3(world_pos_x, height_value, world_pos_y)
insert_into_spatial_hash(Vector3(world_pos_x, height_value, world_pos_y), current_instance_count)
multimesh.set_instance_transform(current_instance_count, transform)
current_instance_count += 1
I'm trying to add a negative scatter item to the tree during runtime. When I add the node I have to rebuild from script, which obviously takes a hot second to finish. It also reshuffles the scattered objects so all my trees "change" locations.
Is the only solution to this to have ScatterItem nodes in the tree but invisible prior to loading the game and change their properties at runtime? I'm asking because this for an RTS.. when a building is placed I want to clear out the foliage around it.. the problem is I don't know how many buildings there will ultimately be.
This is super low priority, I'm just looking for a cleaner way to handle this edge case. I can work around it if necessary, just looking for advice.