Skip to content

Commit

Permalink
Added custom mesh LOD distances and hiding beyond max LOD.
Browse files Browse the repository at this point in the history
Mesh LODs are also updated up to a certain duration per frame and continue
next frame, in case there are lots of different blocks to update
  • Loading branch information
Zylann committed Oct 26, 2023
1 parent 22be792 commit 57d7ef2
Show file tree
Hide file tree
Showing 9 changed files with 392 additions and 116 deletions.
2 changes: 2 additions & 0 deletions doc/source/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ Semver is not yet in place, so each version can have breaking changes, although
- `VoxelLibraryMultiMeshItem `:
- Added `render_layer` property (thanks to m4nu3lf)
- Added `gi_mode` property
- Exposed custom distance ratios for the secondary distance-based LOD system
- Added option to hide instances when beyond their max distance-based LOD (only relevant for terrains with no LOD, or on the last LOD of `VoxelLodTerrain`)
- Node groups on the template scene are now added to instance colliders if present
- `VoxelLodTerrain`:
- Added debug drawing for modifier bounds
Expand Down
9 changes: 6 additions & 3 deletions doc/source/instancing.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,18 @@ To use this, you have to fill the 3 mesh LOD properties on your `VoxelInstanceLi

If only the `mesh` property is set, no LOD will be used.

The distance at which a LOD will be chosen is currently hardcoded, because it depends on the `lod_index` the blocks for that item are loaded into, which in turn depends on the `lod_distance` property of the parent voxel terrain.

![Screenshot of mesh LODs with colors](images/mesh_lods.webp)

If you need fewer LODs, you can assign twice the same mesh. This system is quite rigid because in Godot 4 it might be changed to only have a single slot dedicated to impostor meshes. Indeed, Godot 4 might support LOD on meshes, but it is not planned for the last LODs to become impostors, so this should still be possible to achieve.
If you need fewer LODs, you can assign twice the same mesh.

!!! note
Impostor meshes are simple quads that can fake the presence of the real model over far distances. For example, this is a really fast way to render forests from afar, while being able to use detailed trees when coming closer.

It is possible to customize the distances at which mesh LODs switch, under the `Mesh LOD settings` property group. They are defined as ratios (usually between 0 and 1) relative to the view distance of the corresponding terrain LOD (defined by `lod_index`).

It is also possible to hide instances that fall beyond the maximum distance of the last LOD. This is mainly useful if the terrain has no LOD, however keep in mind that instances will still be generated, they will just not be rendered.


!!! warning
There is currently a performance issue occurring when `MultiMesh.mesh` is changed to a different LOD. It should be easier to fix once [this PR](https://github.com/godotengine/godot/pull/79833) is merged into Godot.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,37 @@ bool VoxelInstanceLibraryMultiMeshItemInspectorPlugin::_zn_can_handle(const Obje
void VoxelInstanceLibraryMultiMeshItemInspectorPlugin::_zn_parse_group(Object *p_object, const String &p_group) {
const VoxelInstanceLibraryMultiMeshItem *item = Object::cast_to<VoxelInstanceLibraryMultiMeshItem>(p_object);
ERR_FAIL_COND(item == nullptr);
if (item->get_scene().is_null()) {
ERR_FAIL_COND(listener == nullptr);
// TODO I preferred this at the end of the group, but Godot doesn't expose anything to do it.
// This is a legacy workflow, we'll see if it can be removed later.
Button *button = memnew(Button);
button->set_tooltip_text(
ZN_TTR("Set properties based on an existing scene. This might copy mesh and material data if "
"the scene embeds them. Properties will not update if the scene changes later."));
button->set_text(ZN_TTR("Update from scene..."));

if (p_group == VoxelInstanceLibraryMultiMeshItem::MANUAL_SETTINGS_GROUP_NAME) {
if (item->get_scene().is_null()) {
ERR_FAIL_COND(listener == nullptr);
// TODO I preferred this at the end of the group, but Godot doesn't expose anything to do it.
// This is a legacy workflow, we'll see if it can be removed later.
Button *button = memnew(Button);
button->set_tooltip_text(
ZN_TTR("Set properties based on an existing scene. This might copy mesh and material data if "
"the scene embeds them. Properties will not update if the scene changes later."));
button->set_text(ZN_TTR("Update from scene..."));
#if defined(ZN_GODOT)
// Using a bind() instead of relying on "currently edited" item in the editor plugin allows to support multiple
// sub-inspectors. Plugins are not instanced per-inspected-object, but custom controls are.
button->connect("pressed",
callable_mp(
listener, &VoxelInstanceLibraryMultiMeshItemEditorPlugin::_on_update_from_scene_button_pressed)
.bind(item));
// Using a bind() instead of relying on "currently edited" item in the editor plugin allows to support
// multiple sub-inspectors. Plugins are not instanced per-inspected-object, but custom controls are.
button->connect("pressed",
callable_mp(listener,
&VoxelInstanceLibraryMultiMeshItemEditorPlugin::_on_update_from_scene_button_pressed)
.bind(item));
#elif defined(ZN_GODOT_EXTENSION)
// TODO GDX: Need to use Callable::bind() but it has no implementation
// See https://github.com/godotengine/godot-cpp/issues/802
ZN_PRINT_ERROR("Unable to setup button to update `VoxelInstanceLibraryMultiMeshItem` from a scene with "
"GDExtension! Callable::bind isn't working.");
// TODO GDX: Need to use Callable::bind() but it has no implementation
// See https://github.com/godotengine/godot-cpp/issues/802
ZN_PRINT_ERROR("Unable to setup button to update `VoxelInstanceLibraryMultiMeshItem` from a scene with "
"GDExtension! Callable::bind isn't working.");
#endif
add_custom_control(button);
return;
}
add_custom_control(button);

if (p_group == VoxelInstanceLibraryMultiMeshItem::MANUAL_SETTINGS_GROUP_NAME) {
Label *label = memnew(Label);
label->set_text(ZN_TTR("Properties are defined by scene."));
add_custom_control(label);
} else {
Label *label = memnew(Label);
label->set_text(ZN_TTR("Properties are defined by the scene property."));
add_custom_control(label);
}
}
// TODO Button to open scene in editor, since Godot doesn't have that in its resource picker menu?
// Perhaps it should rather be a feature request to Godot.
Expand Down
120 changes: 120 additions & 0 deletions terrain/instancing/voxel_instance_library_multimesh_item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "../../util/godot/classes/mesh_instance_3d.h"
#include "../../util/godot/classes/node.h"
#include "../../util/godot/classes/physics_body_3d.h"
#include "../../util/godot/funcs.h"
#include "voxel_instancer.h"

namespace zylann::voxel {
Expand Down Expand Up @@ -63,8 +64,33 @@ void deserialize_group_names(const Array &src, std::vector<StringName> &dst) {
}
}

bool is_ascending(Span<const float> numbers) {
for (unsigned int i = 1; i < numbers.size(); ++i) {
if (numbers[i - 1] > numbers[i]) {
return false;
}
}
return true;
}

bool is_in_range(Span<const float> numbers, float minv, float maxv) {
for (const float v : numbers) {
if (v < minv || v > maxv) {
return false;
}
}
return true;
}

} // namespace

VoxelInstanceLibraryMultiMeshItem::VoxelInstanceLibraryMultiMeshItem() {
_mesh_lod_max_distance_ratios[0] = 0.2;
_mesh_lod_max_distance_ratios[1] = 0.35;
_mesh_lod_max_distance_ratios[2] = 0.6;
_mesh_lod_max_distance_ratios[3] = 1.f;
}

void VoxelInstanceLibraryMultiMeshItem::set_mesh(Ref<Mesh> mesh, int mesh_lod_index) {
Settings &settings = _manual_settings;
ERR_FAIL_INDEX(mesh_lod_index, static_cast<int>(settings.mesh_lods.size()));
Expand All @@ -90,6 +116,24 @@ int VoxelInstanceLibraryMultiMeshItem::get_mesh_lod_count() const {
return _manual_settings.mesh_lod_count;
}

// This version is called when editing in the inspector
void VoxelInstanceLibraryMultiMeshItem::set_mesh_lod_distance_ratio(int mesh_lod_index, float ratio) {
ERR_FAIL_INDEX(mesh_lod_index, static_cast<int>(_mesh_lod_max_distance_ratios.size()));
ratio = math::clamp(ratio, MIN_DISTANCE_RATIO, MAX_DISTANCE_RATIO);
if (mesh_lod_index > 0) {
ratio = math::max(ratio, _mesh_lod_max_distance_ratios[mesh_lod_index - 1]);
}
if (mesh_lod_index + 1 < static_cast<int>(_mesh_lod_max_distance_ratios.size())) {
ratio = math::min(ratio, _mesh_lod_max_distance_ratios[mesh_lod_index + 1]);
}
_mesh_lod_max_distance_ratios[mesh_lod_index] = ratio;
}

float VoxelInstanceLibraryMultiMeshItem::get_mesh_lod_distance_ratio(int mesh_lod_index) const {
ERR_FAIL_INDEX_V(mesh_lod_index, static_cast<int>(_mesh_lod_max_distance_ratios.size()), 0.f);
return _mesh_lod_max_distance_ratios[mesh_lod_index];
}

Ref<Mesh> VoxelInstanceLibraryMultiMeshItem::get_mesh(int mesh_lod_index) const {
const Settings &settings = _manual_settings;
ERR_FAIL_INDEX_V(mesh_lod_index, static_cast<int>(settings.mesh_lods.size()), Ref<Mesh>());
Expand Down Expand Up @@ -388,6 +432,14 @@ Ref<PackedScene> VoxelInstanceLibraryMultiMeshItem::get_scene() const {
return _scene;
}

bool VoxelInstanceLibraryMultiMeshItem::get_hide_beyond_max_lod() const {
return _hide_beyond_max_lod;
}

void VoxelInstanceLibraryMultiMeshItem::set_hide_beyond_max_lod(bool enabled) {
_hide_beyond_max_lod = enabled;
}

const VoxelInstanceLibraryMultiMeshItem::Settings &VoxelInstanceLibraryMultiMeshItem::get_multimesh_settings() const {
if (_scene.is_valid()) {
return _scene_settings;
Expand Down Expand Up @@ -445,6 +497,22 @@ Array VoxelInstanceLibraryMultiMeshItem::_b_get_collision_shapes() const {
return serialize_collision_shape_infos(settings.collision_shapes);
}

PackedFloat32Array VoxelInstanceLibraryMultiMeshItem::_b_get_mesh_lod_distance_ratios() const {
PackedFloat32Array ratios;
copy_to(ratios, to_span(_mesh_lod_max_distance_ratios));
return ratios;
}

// This version is called when loading the resource
void VoxelInstanceLibraryMultiMeshItem::_b_set_mesh_lod_distance_ratios(PackedFloat32Array ratios) {
ZN_ASSERT_RETURN(ratios.size() == _mesh_lod_max_distance_ratios.size());
ZN_ASSERT_RETURN(is_ascending(to_span(ratios)));
if (!is_in_range(to_span(ratios), MIN_DISTANCE_RATIO, MAX_DISTANCE_RATIO)) {
ZN_PRINT_ERROR("LOD distance ratios are not in usual range");
}
copy_to(to_span(_mesh_lod_max_distance_ratios), ratios);
}

void VoxelInstanceLibraryMultiMeshItem::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_mesh", "mesh", "mesh_lod_index"), &VoxelInstanceLibraryMultiMeshItem::set_mesh);
ClassDB::bind_method(D_METHOD("get_mesh", "mesh_lod_index"), &VoxelInstanceLibraryMultiMeshItem::get_mesh);
Expand All @@ -459,6 +527,34 @@ void VoxelInstanceLibraryMultiMeshItem::_bind_methods() {
ClassDB::bind_method(D_METHOD("_get_mesh_lod2"), &VoxelInstanceLibraryMultiMeshItem::_b_get_mesh_lod2);
ClassDB::bind_method(D_METHOD("_get_mesh_lod3"), &VoxelInstanceLibraryMultiMeshItem::_b_get_mesh_lod3);

ClassDB::bind_method(D_METHOD("_get_mesh_lod_distance_ratios"),
&VoxelInstanceLibraryMultiMeshItem::_b_get_mesh_lod_distance_ratios);
ClassDB::bind_method(D_METHOD("_set_mesh_lod_distance_ratios"),
&VoxelInstanceLibraryMultiMeshItem::_b_set_mesh_lod_distance_ratios);

ClassDB::bind_method(D_METHOD("_get_mesh_lod0_distance_ratio"),
&VoxelInstanceLibraryMultiMeshItem::_b_get_mesh_lod0_distance_ratio);
ClassDB::bind_method(D_METHOD("_get_mesh_lod1_distance_ratio"),
&VoxelInstanceLibraryMultiMeshItem::_b_get_mesh_lod1_distance_ratio);
ClassDB::bind_method(D_METHOD("_get_mesh_lod2_distance_ratio"),
&VoxelInstanceLibraryMultiMeshItem::_b_get_mesh_lod2_distance_ratio);
ClassDB::bind_method(D_METHOD("_get_mesh_lod3_distance_ratio"),
&VoxelInstanceLibraryMultiMeshItem::_b_get_mesh_lod3_distance_ratio);

ClassDB::bind_method(D_METHOD("_set_mesh_lod0_distance_ratio", "ratio"),
&VoxelInstanceLibraryMultiMeshItem::_b_set_mesh_lod0_distance_ratio);
ClassDB::bind_method(D_METHOD("_set_mesh_lod1_distance_ratio", "ratio"),
&VoxelInstanceLibraryMultiMeshItem::_b_set_mesh_lod1_distance_ratio);
ClassDB::bind_method(D_METHOD("_set_mesh_lod2_distance_ratio", "ratio"),
&VoxelInstanceLibraryMultiMeshItem::_b_set_mesh_lod2_distance_ratio);
ClassDB::bind_method(D_METHOD("_set_mesh_lod3_distance_ratio", "ratio"),
&VoxelInstanceLibraryMultiMeshItem::_b_set_mesh_lod3_distance_ratio);

ClassDB::bind_method(D_METHOD("set_hide_beyond_max_lod", "enabled"),
&VoxelInstanceLibraryMultiMeshItem::set_hide_beyond_max_lod);
ClassDB::bind_method(
D_METHOD("get_hide_beyond_max_lod"), &VoxelInstanceLibraryMultiMeshItem::get_hide_beyond_max_lod);

ClassDB::bind_method(
D_METHOD("set_render_layer", "render_layer"), &VoxelInstanceLibraryMultiMeshItem::set_render_layer);
ClassDB::bind_method(D_METHOD("get_render_layer"), &VoxelInstanceLibraryMultiMeshItem::get_render_layer);
Expand Down Expand Up @@ -537,6 +633,30 @@ void VoxelInstanceLibraryMultiMeshItem::_bind_methods() {

ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "collision_shapes"), "set_collision_shapes", "get_collision_shapes");

ADD_GROUP("Mesh LOD settings", "");

// Only for editor and scripting
ADD_PROPERTY(
PropertyInfo(Variant::FLOAT, "mesh_lod0_distance_ratio", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR),
"_set_mesh_lod0_distance_ratio", "_get_mesh_lod0_distance_ratio");
ADD_PROPERTY(
PropertyInfo(Variant::FLOAT, "mesh_lod1_distance_ratio", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR),
"_set_mesh_lod1_distance_ratio", "_get_mesh_lod1_distance_ratio");
ADD_PROPERTY(
PropertyInfo(Variant::FLOAT, "mesh_lod2_distance_ratio", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR),
"_set_mesh_lod2_distance_ratio", "_get_mesh_lod2_distance_ratio");
ADD_PROPERTY(
PropertyInfo(Variant::FLOAT, "mesh_lod3_distance_ratio", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR),
"_set_mesh_lod3_distance_ratio", "_get_mesh_lod3_distance_ratio");

// Only for resource serialization
ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT32_ARRAY, "_mesh_lod_distance_ratios", PROPERTY_HINT_NONE, "",
PROPERTY_USAGE_STORAGE),
"_set_mesh_lod_distance_ratios", "_get_mesh_lod_distance_ratios");

ADD_PROPERTY(PropertyInfo(Variant::BOOL, "hide_beyond_max_lod", PROPERTY_HINT_RESOURCE_TYPE),
"set_hide_beyond_max_lod", "get_hide_beyond_max_lod");

BIND_CONSTANT(MAX_MESH_LODS);
}

Expand Down
50 changes: 50 additions & 0 deletions terrain/instancing/voxel_instance_library_multimesh_item.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,24 @@ class VoxelInstanceLibraryMultiMeshItem : public VoxelInstanceLibraryItem {
static const char *MANUAL_SETTINGS_GROUP_NAME;
static const char *SCENE_SETTINGS_GROUP_NAME;

static constexpr float MIN_DISTANCE_RATIO = 0.f;
// Can be higher than 1 because when used with VoxelTerrain it is based on the half-extents of the visible area,
// which is square, so the circular area covered by mesh lods can actually extend a bit further if desired.
static constexpr float MAX_DISTANCE_RATIO = 2.f;

struct CollisionShapeInfo {
Transform3D transform;
Ref<Shape3D> shape;
};

VoxelInstanceLibraryMultiMeshItem();

void set_mesh(Ref<Mesh> mesh, int mesh_lod_index);
Ref<Mesh> get_mesh(int mesh_lod_index) const;

void set_mesh_lod_distance_ratio(int mesh_lod_index, float ratio);
float get_mesh_lod_distance_ratio(int mesh_lod_index) const;

int get_mesh_lod_count() const;

void set_render_layer(int render_layer);
Expand Down Expand Up @@ -60,6 +71,9 @@ class VoxelInstanceLibraryMultiMeshItem : public VoxelInstanceLibraryItem {
void set_scene(Ref<PackedScene> scene);
Ref<PackedScene> get_scene() const;

bool get_hide_beyond_max_lod() const;
void set_hide_beyond_max_lod(bool enabled);

// Internal

struct Settings {
Expand Down Expand Up @@ -89,6 +103,10 @@ class VoxelInstanceLibraryMultiMeshItem : public VoxelInstanceLibraryItem {
return to_span_const(get_multimesh_settings().collision_shapes);
}

inline Span<const float> get_mesh_lod_distance_ratios() const {
return to_span(_mesh_lod_max_distance_ratios);
}

Array serialize_multimesh_item_properties() const;
void deserialize_multimesh_item_properties(Array a);

Expand Down Expand Up @@ -128,6 +146,35 @@ class VoxelInstanceLibraryMultiMeshItem : public VoxelInstanceLibraryItem {
set_mesh(mesh, 3);
}

void _b_set_mesh_lod0_distance_ratio(float ratio) {
set_mesh_lod_distance_ratio(0, ratio);
}
void _b_set_mesh_lod1_distance_ratio(float ratio) {
set_mesh_lod_distance_ratio(1, ratio);
}
void _b_set_mesh_lod2_distance_ratio(float ratio) {
set_mesh_lod_distance_ratio(2, ratio);
}
void _b_set_mesh_lod3_distance_ratio(float ratio) {
set_mesh_lod_distance_ratio(3, ratio);
}

float _b_get_mesh_lod0_distance_ratio() const {
return get_mesh_lod_distance_ratio(0);
}
float _b_get_mesh_lod1_distance_ratio() const {
return get_mesh_lod_distance_ratio(1);
}
float _b_get_mesh_lod2_distance_ratio() const {
return get_mesh_lod_distance_ratio(2);
}
float _b_get_mesh_lod3_distance_ratio() const {
return get_mesh_lod_distance_ratio(3);
}

PackedFloat32Array _b_get_mesh_lod_distance_ratios() const;
void _b_set_mesh_lod_distance_ratios(PackedFloat32Array ratios);

// Settings manually set in the inspector, scripts, and saved to the resource file.
Settings _manual_settings;
// Settings gathered at runtime from the `_scene` property. They are not saved to the resource file. They take
Expand All @@ -142,6 +189,9 @@ class VoxelInstanceLibraryMultiMeshItem : public VoxelInstanceLibraryItem {
// into manual settings, but when Godot saves the resource, it sees the mesh has no dedicated file, so it makes a
// copy of it and embeds it again in the resource.
Ref<PackedScene> _scene;
// This may be used if the terrain has no LOD or the item is on its last LOD
bool _hide_beyond_max_lod = false;
FixedArray<float, MAX_MESH_LODS> _mesh_lod_max_distance_ratios;
};

} // namespace zylann::voxel
Expand Down
Loading

0 comments on commit 57d7ef2

Please sign in to comment.