Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented pooled LOD updating via LODManager. #4

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
* All LOD objects are now updated within a user specified time.
### Changed
* Node Inspector UIs now look more like the ones for built-in nodes, rather than merely scripts.
### Removed
* Refresh times for the LOD nodes.

## 1.0.0 - 2020-09-24

- Initial versioned release.
Expand Down
27 changes: 4 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,31 +125,12 @@ The distance bias (in 3D units) to use for LOD calculations. Positive values
will improve performance at the cost of visual quality, whereas negative values
will improve visual quality at the cost of performance.

### `lod/refresh_rate`
### `lod/refresh_threshold_ms`

*Default:* `0.25`
*Default:* `5`

The rate at which the LOD mesh and particle instances update (in seconds). Lower
values are more reactive but use more CPU. Each LOD instance uses a random
jitter to avoid applying updates on all instances at the same time.

Since meshes and particles are updated in a "discrete" manner rather than a
continuus one, the default refresh rate is quite low. The difference is hardly
visible, yet it helps decrease CPU usage significantly in scenes with hundreds
of instances (or more).

### `lod/light_refresh_rate`

*Default:* `0.05`

The rate at which the LOD light instances update (in seconds). Lower values are
more reactive but use more CPU. Each LOD instance uses a random jitter to avoid
applying updates on all instances at the same time.

Since lights are updated in a "continuous" manner rather than a discrete one,
the default refresh rate is relatively high. Despite not quite being 60 FPS
(`0.01666`), it often looks very close in practice unless the camera is moving
really fast.
How much time can be used to update LOD meshes, lights and particle instances (in milliseconds). Higher
values allow more LODs to be processed at once but uses more CPU.

## Tips and tricks

Expand Down
67 changes: 35 additions & 32 deletions addons/lod/lod_cpu_particles.gd
Original file line number Diff line number Diff line change
@@ -1,59 +1,62 @@
# Copyright © 2020 Hugo Locurcio and contributors - MIT License
# See `LICENSE.md` included in the source distribution for details.
tool
extends CPUParticles
class_name LODCPUParticles, "lod_cpu_particles.svg"

# If `false`, LOD won't update anymore. This can be used for performance comparison
# purposes.
export var enable_lod := true
var enable_lod := true setget set_enable_lod

# The maximum particle emitting distance in units. Past this distance, particles will no longer emit.
export(float, 0.0, 1000.0, 0.1) var max_emit_distance := 50

# The rate at which LODs will be updated (in seconds). Lower values are more reactive
# but use more CPU, which is especially noticeable with large amounts of LOD-enabled nodes.
# Set this accordingly depending on your camera movement speed.
# The default value should suit most projects already.
# Note: Slow cameras don't need to have LOD-enabled objects update their status often.
# This can overridden by setting the project setting `lod/refresh_rate`.
var refresh_rate := 0.25
var max_emit_distance := 50.0

# The LOD bias in units.
# Positive values will decrease the detail level and improve performance.
# Negative values will improve visual appearance at the cost of performance.
# This can overridden by setting the project setting `lod/bias`.
var lod_bias := 0.0

# The internal refresh timer.
var timer := 0.0


func _ready() -> void:
if ProjectSettings.has_setting("lod/particle_bias"):
lod_bias = ProjectSettings.get_setting("lod/particle_bias")
if ProjectSettings.has_setting("lod/refresh_rate"):
refresh_rate = ProjectSettings.get_setting("lod/refresh_rate")

# Add random jitter to the timer to ensure LODs don't all swap at the same time.
randomize()
timer += rand_range(0, refresh_rate)
func set_enable_lod(value: bool) -> void:
enable_lod = value
if is_inside_tree() and Engine.editor_hint:
if enable_lod:
get_tree().root.get_node("LODManager").register_lod_object(self)
else:
get_tree().root.get_node("LODManager").unregister_lod_object(self)

# Despite LOD not being related to physics, we chose to run in `_physics_process()`
# to minimize the amount of method calls per second (and therefore decrease CPU usage).
func _physics_process(delta: float) -> void:
if not enable_lod:
return

func update_lod() -> void:
# We need a camera to do the rest.
var camera := get_viewport().get_camera()
if camera == null:
return

if timer <= refresh_rate:
timer += delta
var distance := camera.global_transform.origin.distance_to(global_transform.origin) + lod_bias
emitting = distance < max_emit_distance


func _get_property_list() -> Array:
var properties := [
{name="LODCPUParticles", type=TYPE_NIL, usage=PROPERTY_USAGE_CATEGORY},
{name="enable_lod", type=TYPE_BOOL},
{name="max_emit_distance", type=TYPE_REAL, hint=PROPERTY_HINT_EXP_RANGE, hint_string="0,1000,0.01,or_greater"},
]
return properties


func _ready() -> void:
if Engine.editor_hint:
return
if ProjectSettings.has_setting("lod/particle_bias"):
lod_bias = ProjectSettings.get_setting("lod/particle_bias")

timer = 0.0
get_tree().root.get_node("LODManager").register_lod_object(self)
update_lod()

var distance := camera.global_transform.origin.distance_to(global_transform.origin) + lod_bias
emitting = distance < max_emit_distance

func _exit_tree() -> void:
if Engine.editor_hint:
return
get_tree().root.get_node("LODManager").unregister_lod_object(self)
51 changes: 51 additions & 0 deletions addons/lod/lod_manager.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright © 2020 Hugo Locurcio and contributors - MIT License
# See `LICENSE.md` included in the source distribution for details.
extends Node

# Registered lod objects.
var lod_objects := []

# Index in the lod array that was last processed.
var current_idx := 0

# Number of lods processed in the current frame. Access this for debugging.
var lods_processed := 0

# How much time can be used to process lods in milliseconds.
# If not all the lods can be processed within this time, they'll be processed in the next frame.
var refresh_threshold_ms := 5


func _ready() -> void:
if ProjectSettings.has_setting("lod/refresh_threshold_ms"):
refresh_threshold_ms = ProjectSettings.get_setting("lod/refresh_threshold_ms")


func _process(_delta: float) -> void:
if lod_objects.empty():
return

if current_idx >= lod_objects.size():
current_idx = 0

lods_processed = 0
var prev_idx := current_idx
var time := OS.get_ticks_msec()

while OS.get_ticks_msec() - time < refresh_threshold_ms:
lod_objects[current_idx].update_lod()
lods_processed += 1

current_idx = wrapi(current_idx + 1, 0, lod_objects.size())
if prev_idx == current_idx:
break

# Register lod object, if it's not already in the list.
func register_lod_object(object: Spatial) -> void:
if not lod_objects.has(object):
lod_objects.append(object)

# Unregister lod object, if it's still in the list.
func unregister_lod_object(object: Spatial) -> void:
if lod_objects.has(object):
lod_objects.erase(object)
80 changes: 42 additions & 38 deletions addons/lod/lod_omni_light.gd
Original file line number Diff line number Diff line change
@@ -1,76 +1,52 @@
# Copyright © 2020 Hugo Locurcio and contributors - MIT License
# See `LICENSE.md` included in the source distribution for details.
tool
extends OmniLight
class_name LODOmniLight, "lod_omni_light.svg"

# If `false`, LOD won't update anymore. This can be used for performance comparison
# purposes.
export var enable_lod := true
var enable_lod := true

# The maximum shadow distance in units. Past this distance, the shadow will be disabled.
export(float, 0.0, 1000.0, 0.1) var shadow_max_distance := 25
var shadow_max_distance := 25.0

# The distance factor at which the shadow starts fading.
# A value of 0.0 will result in the smoothest transition whereas a value of 1.0 disables fading.
export(float, 0.0, 1.0, 0.1) var shadow_fade_start := 0.8
var shadow_fade_start := 0.8

# The maximum shadow distance in units. Past this distance, the light will be hidden.
export(float, 0.0, 1000.0, 0.1) var light_max_distance := 50
var light_max_distance := 50.0

# The distance factor at which the light starts fading.
# A value of 0.0 will result in the smoothest transition whereas a value of 1.0 disables fading.
export(float, 0.0, 1.0, 0.1) var light_fade_start := 0.8

# The rate at which LODs will be updated (in seconds). Lower values are more reactive
# but use more CPU, which is especially noticeable with large amounts of LOD-enabled nodes.
# Set this accordingly depending on your camera movement speed.
# The default value should suit most projects already.
# Note: Slow cameras don't need to have LOD-enabled objects update their status often.
# By default, lights have their LOD updated faster than other LOD nodes since their
# light/shadow intensity needs to change as smoothly as posible.
# This can overridden by setting the project setting `lod/light_refresh_rate`.
var refresh_rate := 0.05
var light_fade_start := 0.8

# The LOD bias in units.
# Positive values will decrease the detail level and improve performance.
# Negative values will improve visual appearance at the cost of performance.
# This can overridden by setting the project setting `lod/bias`.
var lod_bias := 0.0

# The internal refresh timer.
var timer := 0.0

# The light's energy when it was instanced.
var base_light_energy := light_energy


func _ready() -> void:
if ProjectSettings.has_setting("lod/light_bias"):
lod_bias = ProjectSettings.get_setting("lod/light_bias")
if ProjectSettings.has_setting("lod/light_refresh_rate"):
refresh_rate = ProjectSettings.get_setting("lod/light_refresh_rate")

# Add random jitter to the timer to ensure LODs don't all swap at the same time.
randomize()
timer += rand_range(0, refresh_rate)
func set_enable_lod(value: bool) -> void:
enable_lod = value
if is_inside_tree() and Engine.editor_hint:
if enable_lod:
get_tree().root.get_node("LODManager").register_lod_object(self)
else:
get_tree().root.get_node("LODManager").unregister_lod_object(self)

# Despite LOD not being related to physics, we chose to run in `_physics_process()`
# to minimize the amount of method calls per second (and therefore decrease CPU usage).
func _physics_process(delta: float) -> void:
if not enable_lod:
return

func update_lod() -> void:
# We need a camera to do the rest.
var camera := get_viewport().get_camera()
if camera == null:
return

if timer <= refresh_rate:
timer += delta
return

timer = 0.0

var distance := camera.global_transform.origin.distance_to(global_transform.origin) + lod_bias

visible = distance < light_max_distance
Expand All @@ -90,3 +66,31 @@ func _physics_process(delta: float) -> void:
# We're close enough to the light to show its shadow at full darkness.
shadow_value = 0.0
shadow_color = Color(shadow_value, shadow_value, shadow_value)


func _get_property_list() -> Array:
var properties := [
{name="LODOmniLight", type=TYPE_NIL, usage=PROPERTY_USAGE_CATEGORY},
{name="enable_lod", type=TYPE_BOOL},
{name="shadow_max_distance", type=TYPE_REAL, hint=PROPERTY_HINT_EXP_RANGE, hint_string="0,1000,0.01,or_greater"},
{name="shadow_fade_start", type=TYPE_REAL, hint=PROPERTY_HINT_RANGE, hint_string="0,1,0.01"},
{name="light_max_distance", type=TYPE_REAL, hint=PROPERTY_HINT_EXP_RANGE, hint_string="0,1000,0.01,or_greater"},
{name="light_fade_start", type=TYPE_REAL, hint=PROPERTY_HINT_RANGE, hint_string="0,1,0.01"},
]
return properties


func _ready() -> void:
if Engine.editor_hint:
return
if ProjectSettings.has_setting("lod/light_bias"):
lod_bias = ProjectSettings.get_setting("lod/light_bias")

get_tree().root.get_node("LODManager").register_lod_object(self)
update_lod()


func _exit_tree() -> void:
if Engine.editor_hint:
return
get_tree().root.get_node("LODManager").unregister_lod_object(self)
Loading