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

WIP: Make collision generation dynamic #278

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ba18b2f
terrain3d: Enable splitting of collision regions
lw64 Dec 16, 2023
de592db
terrain: Make collision dynamic
lw64 Dec 17, 2023
ab616ac
terrain: Make dynamic collision more efficient and fix bug
lw64 Dec 17, 2023
4c7b7c5
Fix issues and implement collision shape caching
lw64 Dec 31, 2023
a5f3850
Use enum collision_mode instead of individual bools
lw64 Jan 3, 2024
6807c9a
Adjust types, rename & rearrange vars/functions
TokisanGames Jan 3, 2024
f5842a0
Rebuild collision properly when changing modes
lw64 Jan 13, 2024
efad1e7
Wip fix error, still not working though
lw64 Jan 13, 2024
553da7a
Fixup rebase
lw64 Jan 13, 2024
ecc5292
Fix doubled collision shapes
lw64 Jan 17, 2024
25c9c5a
Finish non-editor dynamic collision
lw64 Jan 17, 2024
5102686
Update variables, comments
TokisanGames Jan 17, 2024
f970779
Enforce dynamic variable sanity
TokisanGames Jan 19, 2024
d720d55
Enforce collision initialization through build_collision. Strip camer…
TokisanGames Jan 19, 2024
2ba52c8
WIP: Rewrite dynamic collision
lw64 Feb 7, 2024
f9da3c6
Use memdelete() instead of memfree()
lw64 Mar 13, 2024
d3b097f
Fix off-center issue and rename some variables
lw64 Mar 13, 2024
be00711
Fix editor mode
lw64 Mar 26, 2024
90d78e9
add chunk-system
lw64 Mar 27, 2024
4556f8f
Finish chunked dynamic collisions
lw64 Apr 5, 2024
f73f4ca
Fix chunk snapping
lw64 May 9, 2024
d7c0944
Small fixes
lw64 May 9, 2024
623f9c7
Generate collision with correct height
lw64 May 10, 2024
cf717d8
Rename CollisionChunk to EditorCollisionChunk
lw64 May 10, 2024
0efbc88
Split out fill_map function
lw64 May 10, 2024
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
297 changes: 206 additions & 91 deletions src/terrain_3d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -293,123 +293,201 @@ void Terrain3D::_update_collision() {

int time = Time::get_singleton()->get_ticks_msec();
int region_size = _storage->get_region_size();
int shape_size = region_size + 1;
int collision_shape_size = region_size;
if (_dynamic_collision) {
if (!UtilityFunctions::is_instance_valid(_camera) || !_camera->is_inside_tree()) {
return;
}
collision_shape_size = _collision_dynamic_shape_size;
}
int shapes_per_region = region_size / collision_shape_size;
int shape_size = collision_shape_size + 1;
float hole_const = NAN;
if (ProjectSettings::get_singleton()->get_setting("physics/3d/physics_engine") == "JoltPhysics3D") {
hole_const = __FLT_MAX__;
}

for (int i = 0; i < _storage->get_region_count(); i++) {
PackedFloat32Array map_data = PackedFloat32Array();
map_data.resize(shape_size * shape_size);

Vector2i global_offset = Vector2i(_storage->get_region_offsets()[i]) * region_size;
Vector3 global_pos = Vector3(global_offset.x, 0, global_offset.y);

Ref<Image> map, map_x, map_z, map_xz;
Ref<Image> cmap, cmap_x, cmap_z, cmap_xz;
map = _storage->get_map_region(Terrain3DStorage::TYPE_HEIGHT, i);
cmap = _storage->get_map_region(Terrain3DStorage::TYPE_CONTROL, i);
int region = _storage->get_region_index(Vector3(global_pos.x + region_size, 0, global_pos.z));
if (region >= 0) {
map_x = _storage->get_map_region(Terrain3DStorage::TYPE_HEIGHT, region);
cmap_x = _storage->get_map_region(Terrain3DStorage::TYPE_CONTROL, region);
}
region = _storage->get_region_index(Vector3(global_pos.x, 0, global_pos.z + region_size));
if (region >= 0) {
map_z = _storage->get_map_region(Terrain3DStorage::TYPE_HEIGHT, region);
cmap_z = _storage->get_map_region(Terrain3DStorage::TYPE_CONTROL, region);
Vector3 camera_pos;
Array existing = Array();
if (_dynamic_collision) {
camera_pos = _camera->get_global_position();
camera_pos.y = 0.0f;
}

// destroy previous collision shapes
if (_debug_static_body != nullptr) {
for (int i = _debug_static_body->get_child_count() - 1; i >= 0; i--) {
Node3D *child = (Node3D *)_debug_static_body->get_child(i);
Vector3 child_pos = child->get_global_position();
if (_dynamic_collision && child_pos.distance_to(camera_pos) <= _collision_dynamic_distance) {
existing.append(child_pos);
continue;
}
LOG(DEBUG, "Freeing dsb child ", i, " ", child->get_name());
_debug_static_body->remove_child(child);
_unused_collision_shapes.append(child);
}
region = _storage->get_region_index(Vector3(global_pos.x + region_size, 0, global_pos.z + region_size));
if (region >= 0) {
map_xz = _storage->get_map_region(Terrain3DStorage::TYPE_HEIGHT, region);
cmap_xz = _storage->get_map_region(Terrain3DStorage::TYPE_CONTROL, region);
}
if (_static_body.is_valid() && PhysicsServer3D::get_singleton()->body_get_shape_count(_static_body) > 0) {
for (int i = PhysicsServer3D::get_singleton()->body_get_shape_count(_static_body) - 1; i >= 0; i--) {
RID shape = PhysicsServer3D::get_singleton()->body_get_shape(_static_body, i);
Vector3 position = PhysicsServer3D::get_singleton()->body_get_shape_transform(_static_body, i).origin;
if (_dynamic_collision && position.distance_to(camera_pos) <= _collision_dynamic_distance) {
existing.append(position);
continue;
}
_unused_collision_shapes.append(shape);
}
}

for (int i = 0; i < _storage->get_region_count(); i++) {
for (int jx = 0; jx < shapes_per_region; jx++) {
for (int jz = 0; jz < shapes_per_region; jz++) {
Vector2i global_offset = Vector2i(_storage->get_region_offsets()[i]) * region_size + Vector2i(collision_shape_size * jx, collision_shape_size * jz);
Vector3 global_pos = Vector3(global_offset.x, 0, global_offset.y);
Vector3 global_middle_pos = global_pos + Vector3(collision_shape_size, 0, collision_shape_size) * .5;

if (_dynamic_collision && (global_middle_pos.distance_to(camera_pos) > _collision_dynamic_distance || existing.has(global_middle_pos))) {
continue;
}

for (int z = 0; z < shape_size; z++) {
for (int x = 0; x < shape_size; x++) {
// Choose array indexing to match triangulation of heightmapshape with the mesh
// https://stackoverflow.com/questions/16684856/rotating-a-2d-pixel-array-by-90-degrees
// Normal array index rotated Y=0 - shape rotation Y=0 (xform below)
// int index = z * shape_size + x;
// Array Index Rotated Y=-90 - must rotate shape Y=+90 (xform below)
int index = shape_size - 1 - z + x * shape_size;

// Set heights on local map, or adjacent maps if on the last row/col
if (x < region_size && z < region_size) {
map_data[index] = (Util::is_hole(cmap->get_pixel(x, z).r)) ? hole_const : map->get_pixel(x, z).r;
} else if (x == region_size && z < region_size) {
if (map_x.is_valid()) {
map_data[index] = (Util::is_hole(cmap_x->get_pixel(0, z).r)) ? hole_const : map_x->get_pixel(0, z).r;
PackedFloat32Array map_data;
CollisionShape3D *debug_col_shape = nullptr;
RID col_shape = RID();
if (_dynamic_collision && !_unused_collision_shapes.is_empty()) {
if (!_show_debug_collision) {
col_shape = _unused_collision_shapes.pop_back();
Dictionary shape_data = PhysicsServer3D::get_singleton()->shape_get_data(col_shape);
map_data = shape_data["heights"];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not entirely sure this avoids allocations. But I also don't see another method. the map_data gets assigned back later again.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The allocations and deallocations we want to avoid in update_collision are memnew and memfree. Creating a PackedFloat32Array here is fine. This gets created on the stack which is designed for frequent, rapid creation/destruction. The other two allocate memory on the heap, which should not be as frequent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which other two? Dictionary?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By other two, I mean the general usage of memnew and memfree. Not referring to lines specifically.

} else {
map_data[index] = 0.0f;
debug_col_shape = Object::cast_to<CollisionShape3D>(_unused_collision_shapes.pop_back());
Ref<HeightMapShape3D> hshape = debug_col_shape->get_shape();
map_data = hshape->get_map_data();
}
} else {
map_data = PackedFloat32Array();
map_data.resize(shape_size * shape_size);
}

Ref<Image> map, map_x, map_z, map_xz;
Ref<Image> cmap, cmap_x, cmap_z, cmap_xz;
map = _storage->get_map_region(Terrain3DStorage::TYPE_HEIGHT, i);
cmap = _storage->get_map_region(Terrain3DStorage::TYPE_CONTROL, i);
int region = _storage->get_region_index(Vector3(global_pos.x + region_size, 0, global_pos.z));
if (region >= 0) {
map_x = _storage->get_map_region(Terrain3DStorage::TYPE_HEIGHT, region);
cmap_x = _storage->get_map_region(Terrain3DStorage::TYPE_CONTROL, region);
}
region = _storage->get_region_index(Vector3(global_pos.x, 0, global_pos.z + region_size));
if (region >= 0) {
map_z = _storage->get_map_region(Terrain3DStorage::TYPE_HEIGHT, region);
cmap_z = _storage->get_map_region(Terrain3DStorage::TYPE_CONTROL, region);
}
region = _storage->get_region_index(Vector3(global_pos.x + region_size, 0, global_pos.z + region_size));
if (region >= 0) {
map_xz = _storage->get_map_region(Terrain3DStorage::TYPE_HEIGHT, region);
cmap_xz = _storage->get_map_region(Terrain3DStorage::TYPE_CONTROL, region);
}

for (int z = 0; z < shape_size; z++) {
for (int x = 0; x < shape_size; x++) {
// Choose array indexing to match triangulation of heightmapshape with the mesh
// https://stackoverflow.com/questions/16684856/rotating-a-2d-pixel-array-by-90-degrees
// Normal array index rotated Y=0 - shape rotation Y=0 (xform below)
// int index = z * shape_size + x;
// Array Index Rotated Y=-90 - must rotate shape Y=+90 (xform below)
int index = shape_size - 1 - z + x * shape_size;

int shape_global_x = x + jx * collision_shape_size;
int shape_global_z = z + jz * collision_shape_size;
// Set heights on local map, or adjacent maps if on the last row/col
if (shape_global_x < region_size && shape_global_z < region_size) {
map_data[index] = (Util::is_hole(cmap->get_pixel(shape_global_x, shape_global_z).r)) ? hole_const : map->get_pixel(shape_global_x, shape_global_z).r;
Copy link
Owner

@TokisanGames TokisanGames Feb 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As soon as I switched to Dynamic / Editor running the demo crashes on this line. The camera is outside of the terrain. The reason is:
get_pixel is called with (896, 464), however cmap, cmap_x, cmap_xz, cmap_z are all null

Region is currently -1, which means no region. So if region == -1 after line 427, you can skip all of the code down to L558 probably. Though you may want to disable that shape.

That should allow shapes to be generated on the terrain if the player is near the sculpted regions, just not in the areas outside of that.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this in L428 is sufficient to have it work. However I haven't looked yet to see if this properly disables the shapes or if it needs to do more.

				if (region < 0) {
					continue;
				}

} else if (shape_global_x == region_size && shape_global_z < region_size) {
if (map_x.is_valid()) {
map_data[index] = (Util::is_hole(cmap_x->get_pixel(0, shape_global_z).r)) ? hole_const : map_x->get_pixel(0, shape_global_z).r;
} else {
map_data[index] = 0.0f;
}
} else if (shape_global_z == region_size && shape_global_x < region_size) {
if (map_z.is_valid()) {
map_data[index] = (Util::is_hole(cmap_z->get_pixel(shape_global_x, 0).r)) ? hole_const : map_z->get_pixel(shape_global_x, 0).r;
} else {
map_data[index] = 0.0f;
}
} else if (shape_global_x == region_size && shape_global_z == region_size) {
if (map_xz.is_valid()) {
map_data[index] = (Util::is_hole(cmap_xz->get_pixel(0, 0).r)) ? hole_const : map_xz->get_pixel(0, 0).r;
} else {
map_data[index] = 0.0f;
}
}
}
} else if (z == region_size && x < region_size) {
if (map_z.is_valid()) {
map_data[index] = (Util::is_hole(cmap_z->get_pixel(x, 0).r)) ? hole_const : map_z->get_pixel(x, 0).r;
}

// Non rotated shape for normal array index above
//Transform3D xform = Transform3D(Basis(), global_pos);
// Rotated shape Y=90 for -90 rotated array index
Transform3D xform = Transform3D(Basis(Vector3(0, 1.0, 0), Math_PI * .5), global_middle_pos);

if (!_show_debug_collision) {
if (_dynamic_collision && col_shape.is_valid()) {
} else {
map_data[index] = 0.0f;
col_shape = PhysicsServer3D::get_singleton()->heightmap_shape_create();
Dictionary shape_data;
shape_data["width"] = shape_size;
shape_data["depth"] = shape_size;
shape_data["heights"] = map_data;
Vector2 min_max = _storage->get_height_range();
shape_data["min_height"] = min_max.x;
shape_data["max_height"] = min_max.y;
PhysicsServer3D::get_singleton()->shape_set_data(col_shape, shape_data);
}
} else if (x == region_size && z == region_size) {
if (map_xz.is_valid()) {
map_data[index] = (Util::is_hole(cmap_xz->get_pixel(0, 0).r)) ? hole_const : map_xz->get_pixel(0, 0).r;
int shape_id = PhysicsServer3D::get_singleton()->body_get_shape_count(_static_body);
PhysicsServer3D::get_singleton()->body_set_shape(_static_body, shape_id, col_shape);
PhysicsServer3D::get_singleton()->body_set_shape_transform(_static_body, shape_id, xform);
lw64 marked this conversation as resolved.
Show resolved Hide resolved
PhysicsServer3D::get_singleton()->body_set_collision_mask(_static_body, _collision_mask);
PhysicsServer3D::get_singleton()->body_set_collision_layer(_static_body, _collision_layer);
PhysicsServer3D::get_singleton()->body_set_collision_priority(_static_body, _collision_priority);
} else {
Ref<HeightMapShape3D> hshape;
if (_dynamic_collision && debug_col_shape != nullptr) {
hshape = debug_col_shape->get_shape();
lw64 marked this conversation as resolved.
Show resolved Hide resolved
} else {
map_data[index] = 0.0f;
debug_col_shape = memnew(CollisionShape3D);
debug_col_shape->set_name("CollisionShape3D");
hshape.instantiate();
hshape->set_map_width(shape_size);
hshape->set_map_depth(shape_size);
debug_col_shape->set_shape(hshape);
}
_debug_static_body->add_child(debug_col_shape, true);
debug_col_shape->set_owner(this);
hshape->set_map_data(map_data);
debug_col_shape->set_global_transform(xform);
_debug_static_body->set_collision_mask(_collision_mask);
_debug_static_body->set_collision_layer(_collision_layer);
_debug_static_body->set_collision_priority(_collision_priority);
}
}
}

// Non rotated shape for normal array index above
//Transform3D xform = Transform3D(Basis(), global_pos);
// Rotated shape Y=90 for -90 rotated array index
Transform3D xform = Transform3D(Basis(Vector3(0, 1.0, 0), Math_PI * .5),
global_pos + Vector3(region_size, 0, region_size) * .5);

if (!_show_debug_collision) {
RID shape = PhysicsServer3D::get_singleton()->heightmap_shape_create();
Dictionary shape_data;
shape_data["width"] = shape_size;
shape_data["depth"] = shape_size;
shape_data["heights"] = map_data;
Vector2 min_max = _storage->get_height_range();
shape_data["min_height"] = min_max.x;
shape_data["max_height"] = min_max.y;
PhysicsServer3D::get_singleton()->shape_set_data(shape, shape_data);
PhysicsServer3D::get_singleton()->body_add_shape(_static_body, shape);
PhysicsServer3D::get_singleton()->body_set_shape_transform(_static_body, i, xform);
PhysicsServer3D::get_singleton()->body_set_collision_mask(_static_body, _collision_mask);
PhysicsServer3D::get_singleton()->body_set_collision_layer(_static_body, _collision_layer);
PhysicsServer3D::get_singleton()->body_set_collision_priority(_static_body, _collision_priority);
} else {
CollisionShape3D *debug_col_shape;
debug_col_shape = memnew(CollisionShape3D);
debug_col_shape->set_name("CollisionShape3D");
_debug_static_body->add_child(debug_col_shape, true);
debug_col_shape->set_owner(this);

Ref<HeightMapShape3D> hshape;
hshape.instantiate();
hshape->set_map_width(shape_size);
hshape->set_map_depth(shape_size);
hshape->set_map_data(map_data);
debug_col_shape->set_shape(hshape);
debug_col_shape->set_global_transform(xform);
_debug_static_body->set_collision_mask(_collision_mask);
_debug_static_body->set_collision_layer(_collision_layer);
_debug_static_body->set_collision_priority(_collision_priority);
}
}
LOG(DEBUG, "Collision creation time: ", Time::get_singleton()->get_ticks_msec() - time, " ms");
}

void Terrain3D::_destroy_collision() {
if (_static_body.is_valid()) {
LOG(INFO, "Freeing physics body");
RID shape = PhysicsServer3D::get_singleton()->body_get_shape(_static_body, 0);
PhysicsServer3D::get_singleton()->free_rid(shape);
for (int i = PhysicsServer3D::get_singleton()->body_get_shape_count(_static_body) - 1; i >= 0; i--) {
RID shape = PhysicsServer3D::get_singleton()->body_get_shape(_static_body, i);
PhysicsServer3D::get_singleton()->free_rid(shape);
}
PhysicsServer3D::get_singleton()->free_rid(_static_body);
_static_body = RID();

for (int i = _unused_collision_shapes.size() - 1; i >= 0; i--) {
PhysicsServer3D::get_singleton()->free_rid(_unused_collision_shapes.pop_at(i));
}
}

if (_debug_static_body != nullptr) {
Expand All @@ -421,6 +499,12 @@ void Terrain3D::_destroy_collision() {
memfree(child);
}

for (int i = _unused_collision_shapes.size() - 1; i >= 0; i--) {
Node *child = Object::cast_to<Node>(_unused_collision_shapes.pop_at(i));
LOG(DEBUG, "Freeing cached dsb child ", i, " ", child->get_name());
memfree(child);
}

LOG(DEBUG, "Freeing static body");
remove_child(_debug_static_body);
memfree(_debug_static_body);
Expand Down Expand Up @@ -693,6 +777,24 @@ void Terrain3D::set_show_debug_collision(bool p_enabled) {
}
}

void Terrain3D::set_dynamic_collision(bool p_enabled) {
LOG(INFO, "Setting dynamic collision: ", p_enabled);
_dynamic_collision = p_enabled;
_update_collision();
}

void Terrain3D::set_collision_dynamic_shape_size(int size) {
LOG(INFO, "Setting collision dynamic shape size: ", size);
_collision_dynamic_shape_size = size;
_update_collision();
}

void Terrain3D::set_collision_dynamic_distance(float distance) {
LOG(INFO, "Setting collision dynamic distance: ", distance);
_collision_dynamic_distance = distance;
_update_collision();
}

/**
* Centers the terrain and LODs on a provided position. Y height is ignored.
*/
Expand Down Expand Up @@ -769,6 +871,10 @@ void Terrain3D::snap(Vector3 p_cam_pos) {
edge++;
}
}

if (_dynamic_collision) {
_update_collision();
}
}

void Terrain3D::update_aabbs() {
Expand Down Expand Up @@ -1045,12 +1151,18 @@ void Terrain3D::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_collision_enabled"), &Terrain3D::get_collision_enabled);
ClassDB::bind_method(D_METHOD("set_show_debug_collision", "enabled"), &Terrain3D::set_show_debug_collision);
ClassDB::bind_method(D_METHOD("get_show_debug_collision"), &Terrain3D::get_show_debug_collision);
ClassDB::bind_method(D_METHOD("set_dynamic_collision", "enabled"), &Terrain3D::set_dynamic_collision);
ClassDB::bind_method(D_METHOD("get_dynamic_collision"), &Terrain3D::get_dynamic_collision);
ClassDB::bind_method(D_METHOD("set_collision_layer", "layers"), &Terrain3D::set_collision_layer);
ClassDB::bind_method(D_METHOD("get_collision_layer"), &Terrain3D::get_collision_layer);
ClassDB::bind_method(D_METHOD("set_collision_mask", "mask"), &Terrain3D::set_collision_mask);
ClassDB::bind_method(D_METHOD("get_collision_mask"), &Terrain3D::get_collision_mask);
ClassDB::bind_method(D_METHOD("set_collision_priority", "priority"), &Terrain3D::set_collision_priority);
ClassDB::bind_method(D_METHOD("get_collision_priority"), &Terrain3D::get_collision_priority);
ClassDB::bind_method(D_METHOD("set_collision_dynamic_shape_size", "size"), &Terrain3D::set_collision_dynamic_shape_size);
ClassDB::bind_method(D_METHOD("get_collision_dynamic_shape_size"), &Terrain3D::get_collision_dynamic_shape_size);
ClassDB::bind_method(D_METHOD("set_collision_dynamic_distance", "distance"), &Terrain3D::set_collision_dynamic_distance);
ClassDB::bind_method(D_METHOD("get_collision_dynamic_distance"), &Terrain3D::get_collision_dynamic_distance);

// Utility functions
ClassDB::bind_static_method("Terrain3D", D_METHOD("get_min_max", "image"), &Util::get_min_max);
Expand All @@ -1075,9 +1187,12 @@ void Terrain3D::_bind_methods() {

ADD_GROUP("Collision", "collision_");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "collision_enabled"), "set_collision_enabled", "get_collision_enabled");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "dynamic_collision"), "set_dynamic_collision", "get_dynamic_collision");
lw64 marked this conversation as resolved.
Show resolved Hide resolved
ADD_PROPERTY(PropertyInfo(Variant::INT, "collision_layer", PROPERTY_HINT_LAYERS_3D_PHYSICS), "set_collision_layer", "get_collision_layer");
ADD_PROPERTY(PropertyInfo(Variant::INT, "collision_mask", PROPERTY_HINT_LAYERS_3D_PHYSICS), "set_collision_mask", "get_collision_mask");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "collision_priority"), "set_collision_priority", "get_collision_priority");
ADD_PROPERTY(PropertyInfo(Variant::INT, "collision_dynamic_shape_size"), "set_collision_dynamic_shape_size", "get_collision_dynamic_shape_size");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "collision_dynamic_distance"), "set_collision_dynamic_distance", "get_collision_dynamic_distance");

ADD_GROUP("Mesh", "mesh_");
ADD_PROPERTY(PropertyInfo(Variant::INT, "mesh_lods", PROPERTY_HINT_RANGE, "1,10,1"), "set_mesh_lods", "get_mesh_lods");
Expand Down
Loading