Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
View Distance Multi Mesh
extends MultiMeshInstance
var ZERO_TRANSFORM = Transform.IDENTITY.scaled(Vector3.ZERO)
# Over how many frames to divide the whole multimesh scan
export var refresh_samples = 20
# How many meshes to display at once inside the area
var display_instances = 3000
# Allow the displayed meshes amount to adjust automatically
export var adjust_displayed_instances_amount = true
# How many frames to wait until adjusting the pool size
export var pool_sampling_rate = 60
# How many instances to add to the pool when we need to grow it beyond it's avg size
export var pool_enlarge_amount = 100
# Store mesh status
var amount_of_meshes
var visible_meshes = {}
var instance_mapping = {}
var transforms = []
# Store pool status
var pool_excess_samples = 0
var current_pool_excess = 0
var cumulative_pool_size = 0
var pool_size_samples = 0
# Store refresh status
var should_refresh
var refresh_sample = 0
var display_area
func _ready():
amount_of_meshes = multimesh.instance_count
# Store transformations
for i in amount_of_meshes:
instance_mapping[i] = -1
visible_meshes[i] = false
# Hide meshes
func _hide_all():
for i in multimesh.instance_count:
multimesh.set_instance_transform(i, ZERO_TRANSFORM)
visible_meshes[i] = false
# Called by signal, expect to collide with an cylinder area
func _player_entered_mesh_area(object):
display_area = object
should_refresh = true
# Called by signal
func _player_left(object):
should_refresh = false
display_area = null
func _process(delta):
if should_refresh:
# Expect a cylinder area
var shape = display_area.shape_owner_get_shape(0,0)
var origin = display_area.global_transform.origin
_show_in_circle(origin, shape.radius, refresh_sample)
refresh_sample += 1
if refresh_sample >= refresh_samples:
refresh_sample = 0
func _show_in_circle(origin, radius, batch_number):
var center = Vector2(origin.x, origin.z)
var radius_squared = radius * radius
# Collect instances that are free to be used
var available_instances = []
var pool_index = 0
for i in display_instances:
if multimesh.get_instance_transform(i) == ZERO_TRANSFORM:
# Instance was not being shown
var transform_origin = multimesh.get_instance_transform(i).origin
var point = Vector2(transform_origin.x, transform_origin.z)
if point.distance_squared_to(center) > radius_squared:
# Instance is outside area and should be hidden
visible_meshes[instance_mapping[i]] = false
# Calculate how many posible transforms to go over this frame
var batch_size = (amount_of_meshes / (refresh_samples + 1))
var batch_start = batch_size * batch_number
var batch_end = batch_start + batch_size
# Show the instances that are inside the area
for i in range(batch_start, batch_end):
if pool_index == available_instances.size():
# no more instances available
if visible_meshes[i]:
# we're already showing that mesh with some instance
var transform_origin = transforms[i].origin
var point = Vector2(transform_origin.x, transform_origin.z)
if point.distance_squared_to(center) <= radius_squared:
# The mesh should be shown
multimesh.set_instance_transform(available_instances[pool_index], transforms[i])
visible_meshes[i] = true
instance_mapping[available_instances[pool_index]] = i
pool_index += 1
# Hide the instances that are no longer in view and weren't recycled this frame
for i in range(pool_index, available_instances.size()):
if multimesh.get_instance_transform(available_instances[i]) != ZERO_TRANSFORM:
multimesh.set_instance_transform(available_instances[i], ZERO_TRANSFORM)
# If we have free instances at the end of the frame, it means we went over
# every posible mesh transform in the multimesh. We want to be a little bit
# below that number, so that we can take advantage of early loop breaking.
if adjust_displayed_instances_amount:
_resize_instance_pool(display_instances, available_instances.size() - pool_index)
func _resize_instance_pool(current_pool_size, unused_instances):
# Keep track of average pool size and unused instances
pool_size_samples += 1
cumulative_pool_size += current_pool_size
pool_excess_samples += 1
current_pool_excess += unused_instances
# Only resize every now and then
if pool_excess_samples >= pool_sampling_rate:
var avg_excess = current_pool_excess / pool_excess_samples
var excess_to_remove = ceil(avg_excess * 0.5)
if excess_to_remove > 0:
# If we need to remove instances then do so
display_instances -= excess_to_remove
# If there's no need to remove instances, check if we need to add some
var avg_pool_size = cumulative_pool_size / pool_size_samples
if display_instances >= avg_pool_size:
# We're over pool size average and need to add more instances
display_instances += pool_enlarge_amount
# We're below pool size average and need to add more instances
display_instances += ceil((avg_pool_size - display_instances) * 0.5)
# Reset excess tracker
pool_excess_samples = 0
current_pool_excess = 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment