Convert instancer MMI nodes to RS
Fix default to false for show instances if no instancer (+21 squashed commit) Squashed commit: [818231f] Use SET_IF_DIFF() [a2cee65] Prevent !is_inside_world() error [a300aac] Fix do not free shared shadow MM [badefa1] Fix clear MA after freeing MMIs [6f64fc0] More null mesh checks (+1 squashed commits) Squashed commits: [c185cd4] Check for invalid mesh before using [c6b22fd] Do not erare the dictionary we are iterating on [3a50b8e] rename _mmi_nodes to _mmi_rids [114352f] Remove commented #include [f8f977b] Fix type for visibility layers and only emit signal if it changes in setter [e4f266b] Clarify data structure and usage [58d141e] Remove traces of _mmi_parent [6b2eb3c] Remove _mmi_parent and dump_tree function. Handle instancer visibility [a996c9c] Rename region MMI nodes and reinstate _mmi_nodes.erase(p_region_loc); [3317ff7] Drop blank line separating associated code [f98a346] Set MMI AABB, not MM AABB, for shadow imposter [e2073d5] Use LOD_0, not LAST_LOD, for instance count updates [99da7a7] Move definition of RID shadow_impostor_source_mm; [18e96f6] Remove surplus MeshMMIDict definition [415ad9e] remove blank line as per clang advice [c4e9016] Add blank line between Godot and T3D headers [4bdb15c] Convert instancer MMI nodes to RS
This commit is contained in:
parent
dd7e34ddeb
commit
dd50bfed30
|
|
@ -141,14 +141,10 @@ void Terrain3D::_build_containers() {
|
|||
_label_parent = memnew(Node3D);
|
||||
_label_parent->set_name("Labels");
|
||||
add_child(_label_parent, true);
|
||||
_mmi_parent = memnew(Node3D);
|
||||
_mmi_parent->set_name("MMI");
|
||||
add_child(_mmi_parent, true);
|
||||
}
|
||||
|
||||
void Terrain3D::_destroy_containers() {
|
||||
memdelete_safely(_label_parent);
|
||||
memdelete_safely(_mmi_parent);
|
||||
}
|
||||
|
||||
void Terrain3D::_destroy_labels() {
|
||||
|
|
|
|||
|
|
@ -93,7 +93,6 @@ private:
|
|||
|
||||
// Parent containers for child nodes
|
||||
Node3D *_label_parent;
|
||||
Node3D *_mmi_parent;
|
||||
|
||||
void _initialize();
|
||||
void __physics_process(const double p_delta);
|
||||
|
|
@ -138,7 +137,6 @@ public:
|
|||
Ref<Terrain3DAssets> get_assets() const { return _assets; }
|
||||
Terrain3DCollision *get_collision() const { return _collision; }
|
||||
Terrain3DInstancer *get_instancer() const { return _instancer; }
|
||||
Node *get_mmi_parent() const { return _mmi_parent; }
|
||||
void set_editor(Terrain3DEditor *p_editor);
|
||||
Terrain3DEditor *get_editor() const { return _editor; }
|
||||
void set_plugin(Object *p_plugin);
|
||||
|
|
@ -188,8 +186,8 @@ public:
|
|||
real_t get_cull_margin() const { return _cull_margin; };
|
||||
void set_free_editor_textures(const bool p_free_textures) { _free_editor_textures = p_free_textures; }
|
||||
bool get_free_editor_textures() const { return _free_editor_textures; };
|
||||
void set_show_instances(const bool p_visible) { _mmi_parent ? _mmi_parent->set_visible(p_visible) : void(); }
|
||||
bool get_show_instances() const { return _mmi_parent ? _mmi_parent->is_visible() : false; }
|
||||
void set_show_instances(const bool p_visible) { _instancer ? _instancer->set_show_instances(p_visible) : void(); }
|
||||
bool get_show_instances() const { return _instancer ? _instancer->get_show_instances() : false; }
|
||||
|
||||
// Utility
|
||||
Vector3 get_intersection(const Vector3 &p_src_pos, const Vector3 &p_direction, const bool p_gpu_mode = false);
|
||||
|
|
|
|||
|
|
@ -544,11 +544,11 @@ void Terrain3DAssets::set_mesh_asset(const int p_id, const Ref<Terrain3DMeshAsse
|
|||
return;
|
||||
}
|
||||
LOG(INFO, "Setting mesh id: ", p_id, ", ", p_mesh_asset);
|
||||
_set_asset(TYPE_MESH, p_id, p_mesh_asset);
|
||||
if (p_mesh_asset.is_null()) {
|
||||
IS_INSTANCER_INIT(VOID);
|
||||
_terrain->get_instancer()->clear_by_mesh(p_id);
|
||||
}
|
||||
_set_asset(TYPE_MESH, p_id, p_mesh_asset);
|
||||
update_mesh_list();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
|
||||
|
||||
#include <godot_cpp/classes/resource_saver.hpp>
|
||||
#include <godot_cpp/classes/world3d.hpp>
|
||||
|
||||
#include "constants.h"
|
||||
#include "logger.h"
|
||||
|
|
@ -22,6 +23,9 @@ void Terrain3DInstancer::_process_updates() {
|
|||
return;
|
||||
}
|
||||
IS_DATA_INIT(VOID);
|
||||
if (!_terrain->is_inside_tree()) {
|
||||
return;
|
||||
}
|
||||
const Terrain3DData *data = _terrain->get_data();
|
||||
TypedArray<Vector2i> region_locations = data->get_region_locations();
|
||||
int mesh_count = _terrain->get_assets()->get_mesh_count();
|
||||
|
|
@ -30,10 +34,11 @@ void Terrain3DInstancer::_process_updates() {
|
|||
bool update_all = false;
|
||||
if (_queued_updates.find({ V2I_MAX, -2 }) != _queued_updates.end()) {
|
||||
destroy();
|
||||
update_all = true;
|
||||
update_all = _show_instances;
|
||||
} else if (_queued_updates.find({ V2I_MAX, -1 }) != _queued_updates.end()) {
|
||||
update_all = true;
|
||||
update_all = _show_instances;
|
||||
}
|
||||
|
||||
if (update_all) {
|
||||
LOG(DEBUG, "Updating all regions, all mesh_ids");
|
||||
for (int i = 0; i < region_locations.size(); i++) {
|
||||
|
|
@ -51,6 +56,7 @@ void Terrain3DInstancer::_process_updates() {
|
|||
}
|
||||
}
|
||||
_queued_updates.clear();
|
||||
_rebuild_ma_meshes();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -97,6 +103,7 @@ void Terrain3DInstancer::_process_updates() {
|
|||
_update_mmi_by_region(region, mesh_id);
|
||||
}
|
||||
_queued_updates.clear();
|
||||
_rebuild_ma_meshes();
|
||||
}
|
||||
|
||||
void Terrain3DInstancer::_update_mmi_by_region(const Terrain3DRegion *p_region, const int p_mesh_id) {
|
||||
|
|
@ -111,16 +118,6 @@ void Terrain3DInstancer::_update_mmi_by_region(const Terrain3DRegion *p_region,
|
|||
Vector2i region_loc = p_region->get_location();
|
||||
Dictionary mesh_inst_dict = p_region->get_instances();
|
||||
|
||||
// Create MMI container if needed (always, per region)
|
||||
String rname("Region" + Util::location_to_string(region_loc));
|
||||
if (_mmi_containers.count(region_loc) == 0) {
|
||||
LOG(DEBUG, "Creating new region MMI container Terrain3D/MMI/", rname);
|
||||
Node3D *node = memnew(Node3D);
|
||||
node->set_name(rname);
|
||||
_mmi_containers[region_loc] = node;
|
||||
_terrain->get_mmi_parent()->add_child(node, true);
|
||||
}
|
||||
|
||||
// Verify mesh id is valid, enabled, and has MeshInstance3Ds
|
||||
Ref<Terrain3DMeshAsset> ma = _terrain->get_assets()->get_mesh_asset(p_mesh_id);
|
||||
if (!ma.is_valid()) {
|
||||
|
|
@ -139,6 +136,12 @@ void Terrain3DInstancer::_update_mmi_by_region(const Terrain3DRegion *p_region,
|
|||
return;
|
||||
}
|
||||
|
||||
if (ma->get_scene_file_changed()) {
|
||||
LOG(EXTREME, "MeshAsset ", p_mesh_id, " has no scene file, destroying MMIs in region ", region_loc);
|
||||
_destroy_mmi_by_location(region_loc, p_mesh_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process cells
|
||||
Dictionary cell_inst_dict = mesh_inst_dict[p_mesh_id];
|
||||
Array cell_locations = cell_inst_dict.keys();
|
||||
|
|
@ -167,6 +170,7 @@ void Terrain3DInstancer::_update_mmi_by_region(const Terrain3DRegion *p_region,
|
|||
LOG(EXTREME, "Destroyed old MMIs mesh ", p_mesh_id, " cell ", cell, ", LOD ", lod);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean Shadow MMIs
|
||||
bool shadow_lod_disabled = (ma->get_shadow_impostor() == 0 ||
|
||||
ma->get_cast_shadows() == SHADOWS_OFF);
|
||||
|
|
@ -178,8 +182,8 @@ void Terrain3DInstancer::_update_mmi_by_region(const Terrain3DRegion *p_region,
|
|||
// Setup MMIs for each LOD + shadows
|
||||
|
||||
// Get or create mesh dict (defined here as cleanup above might invalidate it)
|
||||
MeshMMIDict &mesh_mmi_dict = _mmi_nodes[region_loc];
|
||||
Ref<MultiMesh> shadow_impostor_source_mm;
|
||||
MeshMMIDict &mesh_mmi_dict = _mmi_rids[region_loc];
|
||||
RID shadow_impostor_source_mm;
|
||||
|
||||
for (int lod = ma->get_last_lod(); lod >= Terrain3DMeshAsset::SHADOW_LOD_ID; lod--) {
|
||||
// Don't create shadow MMI if not needed
|
||||
|
|
@ -195,38 +199,23 @@ void Terrain3DInstancer::_update_mmi_by_region(const Terrain3DRegion *p_region,
|
|||
// Get or create MMI - [] creates key if missing
|
||||
Vector2i mesh_key(p_mesh_id, lod);
|
||||
CellMMIDict &cell_mmi_dict = mesh_mmi_dict[mesh_key];
|
||||
MultiMeshInstance3D *mmi = cell_mmi_dict[cell]; // null if missing
|
||||
if (!mmi) {
|
||||
mmi = memnew(MultiMeshInstance3D);
|
||||
LOG(EXTREME, "No MMI found, Created new MultiMeshInstance3D for cell ", cell, ": ", ptr_to_str(mmi));
|
||||
// Node name is MMI3D_Cell##_##_Mesh#_LOD#
|
||||
String cstring = "_C" + Util::location_to_string(cell).trim_prefix("_");
|
||||
String mstring = "_M" + String::num_int64(p_mesh_id);
|
||||
String lstring = "_L" + ((lod == Terrain3DMeshAsset::SHADOW_LOD_ID) ? "S" : String::num_int64(lod));
|
||||
mmi->set_name("MMI3D" + cstring + mstring + lstring);
|
||||
cell_mmi_dict[cell] = mmi;
|
||||
|
||||
//Attach to tree
|
||||
Node *node_container = _terrain->get_mmi_parent()->get_node_internal(rname);
|
||||
if (!node_container) {
|
||||
LOG(ERROR, rname, " isn't attached to the tree.");
|
||||
memdelete(mmi);
|
||||
cell_mmi_dict.erase(cell);
|
||||
continue;
|
||||
}
|
||||
node_container->add_child(mmi, true);
|
||||
RID &mmi = cell_mmi_dict[cell].first; // null if missing
|
||||
if (!mmi.is_valid()) {
|
||||
mmi = RS->instance_create();
|
||||
RS->instance_set_scenario(mmi, _terrain->get_world_3d()->get_scenario());
|
||||
modified = true; // New MMI needs full update
|
||||
}
|
||||
|
||||
// Always update MMI propertiess
|
||||
if (ma->is_highlighted()) {
|
||||
mmi->set_material_override(ma->get_highlight_material());
|
||||
mmi->set_material_overlay(Ref<Material>());
|
||||
RS->instance_geometry_set_material_override(mmi, ma->get_highlight_material().is_valid() ? ma->get_highlight_material()->get_rid() : RID());
|
||||
RS->instance_geometry_set_material_overlay(mmi, RID());
|
||||
} else {
|
||||
mmi->set_material_override(ma->get_material_override());
|
||||
mmi->set_material_overlay(ma->get_material_overlay());
|
||||
RS->instance_geometry_set_material_override(mmi, ma->get_material_override().is_valid() ? ma->get_material_override()->get_rid() : RID());
|
||||
RS->instance_geometry_set_material_overlay(mmi, ma->get_material_overlay().is_valid() ? ma->get_material_overlay()->get_rid() : RID());
|
||||
}
|
||||
mmi->set_cast_shadows_setting(ma->get_lod_cast_shadows(lod));
|
||||
RS->instance_geometry_set_cast_shadows_setting(mmi, ma->get_lod_cast_shadows(lod));
|
||||
RS->instance_set_layer_mask(mmi, ma->get_visibility_layers());
|
||||
_set_mmi_lod_ranges(mmi, ma, lod);
|
||||
|
||||
// Reposition MMI to region location
|
||||
|
|
@ -235,40 +224,37 @@ void Terrain3DInstancer::_update_mmi_by_region(const Terrain3DRegion *p_region,
|
|||
real_t vertex_spacing = _terrain->get_vertex_spacing();
|
||||
t.origin.x += region_loc.x * region_size * vertex_spacing;
|
||||
t.origin.z += region_loc.y * region_size * vertex_spacing;
|
||||
mmi->set_as_top_level(true);
|
||||
mmi->set_global_transform(t);
|
||||
|
||||
RS->instance_set_transform(mmi, t);
|
||||
|
||||
RID &mm = cell_mmi_dict[cell].second;
|
||||
// Only recreate MultiMesh if modified/no existing (with override for shadow)
|
||||
// Always update shadow MMI (source may have changed)
|
||||
if (modified || mmi->get_multimesh().is_null() ||
|
||||
if (modified || !mm.is_valid() ||
|
||||
lod == Terrain3DMeshAsset::SHADOW_LOD_ID) {
|
||||
// Subtract previous instance count of this MMI
|
||||
if (lod == 0) {
|
||||
if (mmi->get_multimesh().is_valid()) {
|
||||
ma->update_instance_count(-mmi->get_multimesh()->get_instance_count());
|
||||
}
|
||||
// Subtract previous instance count for this cell
|
||||
if (mm.is_valid() && lod == 0) {
|
||||
ma->update_instance_count(-RS->multimesh_get_instance_count(mm));
|
||||
}
|
||||
|
||||
Ref<MultiMesh> mm;
|
||||
if (lod == Terrain3DMeshAsset::SHADOW_LOD_ID) {
|
||||
// Reuse impostor LOD MM as shadow impostor
|
||||
mm = shadow_impostor_source_mm;
|
||||
if (mm.is_null()) {
|
||||
if (!mm.is_valid()) {
|
||||
LOG(ERROR, "Shadow MM is null for cell ", cell, " lod ", lod, " xforms: ", xforms.size());
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
mm = _create_multimesh(p_mesh_id, lod, xforms, colors);
|
||||
}
|
||||
if (mm.is_null()) {
|
||||
if (!mm.is_valid()) {
|
||||
LOG(ERROR, "Null MM for cell ", cell, " lod ", lod, " xforms: ", xforms.size());
|
||||
continue;
|
||||
}
|
||||
mmi->set_multimesh(mm);
|
||||
RS->instance_set_base(mmi, mm);
|
||||
|
||||
// Add current instance count of this new MMI
|
||||
// Add current instance count for this cell
|
||||
if (lod == 0) {
|
||||
ma->update_instance_count(mm->get_instance_count());
|
||||
ma->update_instance_count(RS->multimesh_get_instance_count(mm));
|
||||
}
|
||||
|
||||
// Clear modified only for visible LODs
|
||||
|
|
@ -277,40 +263,66 @@ void Terrain3DInstancer::_update_mmi_by_region(const Terrain3DRegion *p_region,
|
|||
}
|
||||
} else {
|
||||
// Needed to update generated mesh changes
|
||||
mmi->get_multimesh()->set_mesh(ma->get_mesh(lod));
|
||||
Ref<Mesh> mesh = ma->get_mesh(lod);
|
||||
if (!mesh.is_valid()) {
|
||||
LOG(ERROR, "Mesh is null for LOD ", lod);
|
||||
RS->free_rid(mmi);
|
||||
RS->free_rid(mm);
|
||||
continue;
|
||||
}
|
||||
RS->multimesh_set_mesh(mm, mesh->get_rid());
|
||||
}
|
||||
// Capture source MM from shadow impostor LOD
|
||||
if (lod == ma->get_shadow_impostor()) {
|
||||
shadow_impostor_source_mm = mmi->get_multimesh();
|
||||
shadow_impostor_source_mm = mm;
|
||||
}
|
||||
} // End for LOD loop
|
||||
|
||||
// Set all LOD mmi AABB to match LOD0 to ensure no gaps between transitions.
|
||||
AABB mmi_custom_aabb;
|
||||
AABB mm_custom_aabb;
|
||||
for (int lod = 0; lod <= ma->get_last_lod(); lod++) {
|
||||
Vector2i mesh_key(p_mesh_id, lod);
|
||||
CellMMIDict &cell_mmi_dict = mesh_mmi_dict[mesh_key];
|
||||
MultiMeshInstance3D *mmi = cell_mmi_dict[cell];
|
||||
if (mmi) {
|
||||
RID &mmi = cell_mmi_dict[cell].first;
|
||||
RID &mm = cell_mmi_dict[cell].second;
|
||||
if (mm.is_valid() && mmi.is_valid()) {
|
||||
if (lod == 0) {
|
||||
mmi_custom_aabb = mmi->get_aabb();
|
||||
mm_custom_aabb = RS->multimesh_get_aabb(mm);
|
||||
} else {
|
||||
mmi->set_custom_aabb(mmi_custom_aabb);
|
||||
RS->multimesh_set_custom_aabb(mm, mm_custom_aabb);
|
||||
}
|
||||
RS->instance_set_custom_aabb(mmi, mm_custom_aabb);
|
||||
}
|
||||
}
|
||||
if (ma->get_shadow_impostor() > 0) {
|
||||
Vector2i mesh_key(p_mesh_id, Terrain3DMeshAsset::SHADOW_LOD_ID);
|
||||
CellMMIDict &cell_mmi_dict = mesh_mmi_dict[mesh_key];
|
||||
MultiMeshInstance3D *mmi = cell_mmi_dict[cell];
|
||||
if (mmi) {
|
||||
mmi->set_custom_aabb(mmi_custom_aabb);
|
||||
RID &mmi = cell_mmi_dict[cell].first;
|
||||
if (mmi.is_valid()) {
|
||||
RS->instance_set_custom_aabb(mmi, mm_custom_aabb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Terrain3DInstancer::_set_mmi_lod_ranges(MultiMeshInstance3D *p_mmi, const Ref<Terrain3DMeshAsset> &p_ma, const int p_lod) {
|
||||
void Terrain3DInstancer::_rebuild_ma_meshes() {
|
||||
// Rebuild all meshes for all mesh assets that have changed
|
||||
int mesh_count = _terrain->get_assets()->get_mesh_count();
|
||||
for (int mesh_id = 0; mesh_id < mesh_count; mesh_id++) {
|
||||
Ref<Terrain3DMeshAsset> ma = _terrain->get_assets()->get_mesh_asset(mesh_id);
|
||||
if (ma.is_valid() && ma->get_scene_file_changed()) {
|
||||
LOG(DEBUG, "Rebuilding meshes for MeshAsset ID: ", mesh_id);
|
||||
if (ma->get_scene_file().is_valid()) {
|
||||
ma->parse_scene_meshes();
|
||||
} else {
|
||||
ma->set_generated_type(Terrain3DMeshAsset::TYPE_TEXTURE_CARD);
|
||||
}
|
||||
ma->set_scene_file_changed(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Terrain3DInstancer::_set_mmi_lod_ranges(RID p_mmi, const Ref<Terrain3DMeshAsset> &p_ma, const int p_lod) {
|
||||
if (!p_mmi || p_ma.is_null()) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -326,17 +338,12 @@ void Terrain3DInstancer::_set_mmi_lod_ranges(MultiMeshInstance3D *p_mmi, const R
|
|||
if (margin > 0.f) {
|
||||
lod_begin = MAX(lod_begin < 0.001f ? 0.f : lod_begin - margin, 0.f);
|
||||
lod_end = MAX(lod_end < 0.001f ? 0.f : lod_end + margin, 0.f);
|
||||
p_mmi->set_visibility_range_begin_margin(lod_begin < 0.001f ? 0.f : margin);
|
||||
p_mmi->set_visibility_range_end_margin(lod_end < 0.001f ? 0.f : margin);
|
||||
p_mmi->set_visibility_range_fade_mode(GeometryInstance3D::VISIBILITY_RANGE_FADE_SELF);
|
||||
real_t begin_margin = lod_begin < 0.001f ? 0.f : margin;
|
||||
real_t end_margin = lod_end < 0.001f ? 0.f : margin;
|
||||
RS->instance_geometry_set_visibility_range(p_mmi, lod_begin, lod_end, begin_margin, end_margin, RenderingServer::VISIBILITY_RANGE_FADE_SELF);
|
||||
} else {
|
||||
// Fade mode unset (Godot default)
|
||||
p_mmi->set_visibility_range_begin_margin(0.f);
|
||||
p_mmi->set_visibility_range_end_margin(0.f);
|
||||
p_mmi->set_visibility_range_fade_mode(GeometryInstance3D::VISIBILITY_RANGE_FADE_DISABLED);
|
||||
RS->instance_geometry_set_visibility_range(p_mmi, lod_begin, lod_end, 0.f, 0.f, RenderingServer::VISIBILITY_RANGE_FADE_DISABLED);
|
||||
}
|
||||
p_mmi->set_visibility_range_begin(lod_begin);
|
||||
p_mmi->set_visibility_range_end(lod_end);
|
||||
}
|
||||
|
||||
void Terrain3DInstancer::_update_vertex_spacing(const real_t p_vertex_spacing) {
|
||||
|
|
@ -402,8 +409,8 @@ void Terrain3DInstancer::_destroy_mmi_by_mesh(const int p_mesh_id) {
|
|||
void Terrain3DInstancer::_destroy_mmi_by_location(const Vector2i &p_region_loc, const int p_mesh_id) {
|
||||
LOG(DEBUG, "Deleting all MMIs in region: ", p_region_loc, " for mesh_id: ", p_mesh_id);
|
||||
std::unordered_set<Vector2i, Vector2iHash> cells;
|
||||
if (_mmi_nodes.count(p_region_loc) > 0) {
|
||||
MeshMMIDict &mesh_mmi_dict = _mmi_nodes[p_region_loc];
|
||||
if (_mmi_rids.count(p_region_loc) > 0) {
|
||||
MeshMMIDict &mesh_mmi_dict = _mmi_rids[p_region_loc];
|
||||
for (const auto &mesh_entry : mesh_mmi_dict) {
|
||||
const Vector2i &mesh_key = mesh_entry.first;
|
||||
if (mesh_key.x != p_mesh_id) {
|
||||
|
|
@ -419,13 +426,18 @@ void Terrain3DInstancer::_destroy_mmi_by_location(const Vector2i &p_region_loc,
|
|||
for (const auto &cell : cells) {
|
||||
_destroy_mmi_by_cell(p_region_loc, p_mesh_id, cell);
|
||||
}
|
||||
|
||||
// After all cells are destroyed, if the region is now empty, erase it
|
||||
if (_mmi_rids.count(p_region_loc) > 0 && _mmi_rids[p_region_loc].empty()) {
|
||||
_mmi_rids.erase(p_region_loc);
|
||||
}
|
||||
}
|
||||
|
||||
void Terrain3DInstancer::_destroy_mmi_by_cell(const Vector2i &p_region_loc, const int p_mesh_id, const Vector2i p_cell, const int p_lod) {
|
||||
if (_mmi_nodes.count(p_region_loc) == 0) {
|
||||
if (_mmi_rids.count(p_region_loc) == 0) {
|
||||
return;
|
||||
}
|
||||
MeshMMIDict &mesh_mmi_dict = _mmi_nodes[p_region_loc];
|
||||
MeshMMIDict &mesh_mmi_dict = _mmi_rids[p_region_loc];
|
||||
Ref<Terrain3DMeshAsset> ma = _terrain->get_assets()->get_mesh_asset(p_mesh_id);
|
||||
|
||||
for (int lod = Terrain3DMeshAsset::SHADOW_LOD_ID; lod < Terrain3DMeshAsset::MAX_LOD_COUNT; lod++) {
|
||||
|
|
@ -441,37 +453,27 @@ void Terrain3DInstancer::_destroy_mmi_by_cell(const Vector2i &p_region_loc, cons
|
|||
if (cell_mmi_dict.count(p_cell) == 0) {
|
||||
continue;
|
||||
}
|
||||
MultiMeshInstance3D *mmi = cell_mmi_dict[p_cell];
|
||||
if (ma.is_valid() && mmi && mmi->get_multimesh().is_valid()) {
|
||||
|
||||
RID &mmi = cell_mmi_dict[p_cell].first;
|
||||
RID &mm = cell_mmi_dict[p_cell].second;
|
||||
if (ma.is_valid() && mm.is_valid()) {
|
||||
if (lod == 0) {
|
||||
ma->update_instance_count(-mmi->get_multimesh()->get_instance_count());
|
||||
ma->update_instance_count(-RS->multimesh_get_instance_count(mm));
|
||||
}
|
||||
}
|
||||
LOG(EXTREME, "Freeing ", ptr_to_str(mmi), " and erasing mmi cell ", p_cell);
|
||||
remove_from_tree(mmi);
|
||||
memdelete_safely(mmi);
|
||||
|
||||
LOG(EXTREME, "Freeing ", mmi, " and erasing mmi cell ", p_cell);
|
||||
RS->free_rid(mmi);
|
||||
if (lod != Terrain3DMeshAsset::SHADOW_LOD_ID) {
|
||||
RS->free_rid(mm);
|
||||
}
|
||||
|
||||
cell_mmi_dict.erase(p_cell);
|
||||
if (cell_mmi_dict.empty()) {
|
||||
LOG(EXTREME, "Removing mesh ", mesh_key, " from cell MMI dictionary");
|
||||
mesh_mmi_dict.erase(mesh_key); // invalidates cell_mmi_dict
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up region if we've removed the last MMI and cell
|
||||
if (mesh_mmi_dict.empty()) {
|
||||
LOG(EXTREME, "Removing region ", p_region_loc, " from mesh MMI dictionary");
|
||||
if (_mmi_containers.count(p_region_loc) > 0) {
|
||||
Node *node = _mmi_containers[p_region_loc];
|
||||
if (node && node->get_child_count() == 0) {
|
||||
LOG(EXTREME, "Removing ", node->get_name());
|
||||
_mmi_containers.erase(p_region_loc);
|
||||
remove_from_tree(node);
|
||||
memdelete_safely(node);
|
||||
}
|
||||
}
|
||||
// This invalidates mesh_mmi_dict here and for calling functions
|
||||
_mmi_nodes.erase(p_region_loc);
|
||||
}
|
||||
}
|
||||
|
||||
void Terrain3DInstancer::_backup_region(const Ref<Terrain3DRegion> &p_region) {
|
||||
|
|
@ -485,8 +487,8 @@ void Terrain3DInstancer::_backup_region(const Ref<Terrain3DRegion> &p_region) {
|
|||
}
|
||||
}
|
||||
|
||||
Ref<MultiMesh> Terrain3DInstancer::_create_multimesh(const int p_mesh_id, const int p_lod, const TypedArray<Transform3D> &p_xforms, const PackedColorArray &p_colors) const {
|
||||
Ref<MultiMesh> mm;
|
||||
RID Terrain3DInstancer::_create_multimesh(const int p_mesh_id, const int p_lod, const TypedArray<Transform3D> &p_xforms, const PackedColorArray &p_colors) const {
|
||||
RID mm;
|
||||
IS_INIT(mm);
|
||||
Ref<Terrain3DMeshAsset> mesh_asset = _terrain->get_assets()->get_mesh_asset(p_mesh_id);
|
||||
if (mesh_asset.is_null()) {
|
||||
|
|
@ -498,17 +500,13 @@ Ref<MultiMesh> Terrain3DInstancer::_create_multimesh(const int p_mesh_id, const
|
|||
LOG(ERROR, "No LOD ", p_lod, " for mesh id ", p_mesh_id, " found. Max: ", mesh_asset->get_lod_count());
|
||||
return mm;
|
||||
}
|
||||
mm.instantiate();
|
||||
mm->set_transform_format(MultiMesh::TRANSFORM_3D);
|
||||
mm->set_use_colors(true);
|
||||
mm->set_mesh(mesh);
|
||||
if (p_xforms.size() > 0) {
|
||||
mm->set_instance_count(p_xforms.size());
|
||||
for (int i = 0; i < p_xforms.size(); i++) {
|
||||
mm->set_instance_transform(i, p_xforms[i]);
|
||||
if (i < p_colors.size()) {
|
||||
mm->set_instance_color(i, p_colors[i]);
|
||||
}
|
||||
mm = RS->multimesh_create();
|
||||
RS->multimesh_allocate_data(mm, p_xforms.size(), RenderingServer::MULTIMESH_TRANSFORM_3D, true, false, false);
|
||||
RS->multimesh_set_mesh(mm, mesh->get_rid());
|
||||
for (int i = 0; i < p_xforms.size(); i++) {
|
||||
RS->multimesh_instance_set_transform(mm, i, p_xforms[i]);
|
||||
if (i < p_colors.size()) {
|
||||
RS->multimesh_instance_set_color(mm, i, p_colors[i]);
|
||||
}
|
||||
}
|
||||
return mm;
|
||||
|
|
@ -1336,24 +1334,10 @@ void Terrain3DInstancer::update_mmis(const int p_mesh_id, const Vector2i &p_regi
|
|||
}
|
||||
}
|
||||
|
||||
void Terrain3DInstancer::dump_mmis() {
|
||||
LOG(WARN, "Dumping MMI tree and node containers");
|
||||
LOG(MESG, "_mmi_containers size: ", int(_mmi_containers.size()));
|
||||
for (auto &it : _mmi_containers) {
|
||||
LOG(MESG, "_mmi_containers region: ", it.first, ", node ptr: ", ptr_to_str(it.second));
|
||||
}
|
||||
LOG(MESG, "_mmi tree: ");
|
||||
_terrain->get_mmi_parent()->print_tree();
|
||||
LOG(MESG, "_mmi_nodes size: ", int(_mmi_nodes.size()));
|
||||
for (auto &i : _mmi_nodes) {
|
||||
LOG(MESG, "_mmi_nodes region: ", i.first, ", dict ptr: ", ptr_to_str(&i.second));
|
||||
for (auto &j : i.second) {
|
||||
LOG(MESG, "mesh_mmi_dict mesh: ", j.first, ", dict ptr: ", ptr_to_str(&j.second));
|
||||
for (auto &k : j.second) {
|
||||
LOG(MESG, "cell_mmi_dict cell: ", k.first, ", mmi ptr: ", ptr_to_str(k.second));
|
||||
}
|
||||
}
|
||||
}
|
||||
void Terrain3DInstancer::set_show_instances(const bool p_visible) {
|
||||
SET_IF_DIFF(_show_instances, p_visible);
|
||||
LOG(INFO, "Setting instancer visibility to: ", p_visible);
|
||||
update_mmis(-1, V2I_MAX, true);
|
||||
}
|
||||
|
||||
///////////////////////////
|
||||
|
|
@ -1374,5 +1358,6 @@ void Terrain3DInstancer::_bind_methods() {
|
|||
ClassDB::bind_method(D_METHOD("get_closest_mesh_id", "global_position"), &Terrain3DInstancer::get_closest_mesh_id);
|
||||
ClassDB::bind_method(D_METHOD("update_mmis", "mesh_id", "region_location", "rebuild_all"), &Terrain3DInstancer::update_mmis, DEFVAL(-1), DEFVAL(V2I_MAX), DEFVAL(false));
|
||||
ClassDB::bind_method(D_METHOD("swap_ids", "src_id", "dest_id"), &Terrain3DInstancer::swap_ids);
|
||||
ClassDB::bind_method(D_METHOD("dump_mmis"), &Terrain3DInstancer::dump_mmis);
|
||||
ClassDB::bind_method(D_METHOD("set_show_instances", "p_visible"), &Terrain3DInstancer::set_show_instances);
|
||||
ClassDB::bind_method(D_METHOD("get_show_instances"), &Terrain3DInstancer::get_show_instances);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,15 +28,12 @@ private:
|
|||
// MM Resources stored in Terrain3DRegion::_instances as
|
||||
// Region::_instances{mesh_id:int} -> cell{v2i} -> [ TypedArray<Transform3D>, PackedColorArray, modified:bool ]
|
||||
|
||||
// MMI Objects attached to tree, freed in destructor, stored as
|
||||
// _mmi_nodes{region_loc} -> mesh{v2i(mesh_id,lod)} -> cell{v2i} -> MultiMeshInstance3D
|
||||
using CellMMIDict = std::unordered_map<Vector2i, MultiMeshInstance3D *, Vector2iHash>;
|
||||
using MeshMMIDict = std::unordered_map<Vector2i, CellMMIDict, Vector2iHash>;
|
||||
std::unordered_map<Vector2i, MeshMMIDict, Vector2iHash> _mmi_nodes;
|
||||
// A pair of MMI and MM RIDs, freed in destructor, stored as
|
||||
// _mmi_rids{region_loc} -> mesh{v2i(mesh_id,lod)} -> cell{v2i} -> std::pair<mmi_RID, mm_RID>
|
||||
|
||||
// Region MMI containers named Terrain3D/MMI/Region* are stored here as
|
||||
// _mmi_containers{region_loc} -> Node3D
|
||||
std::unordered_map<Vector2i, Node3D *, Vector2iHash> _mmi_containers;
|
||||
using CellMMIDict = std::unordered_map<Vector2i, std::pair<RID, RID>, Vector2iHash>;
|
||||
using MeshMMIDict = std::unordered_map<Vector2i, CellMMIDict, Vector2iHash>;
|
||||
std::unordered_map<Vector2i, MeshMMIDict, Vector2iHash> _mmi_rids;
|
||||
|
||||
// MMI Updates tracked in a unique Set of <region_location, mesh_id>
|
||||
// <V2I_MAX, -2> means destroy first, then update everything
|
||||
|
|
@ -47,17 +44,19 @@ private:
|
|||
V2IIntPair _queued_updates;
|
||||
|
||||
uint32_t _density_counter = 0;
|
||||
bool _show_instances = true;
|
||||
|
||||
uint32_t _get_density_count(const real_t p_density);
|
||||
void _process_updates();
|
||||
void _update_mmi_by_region(const Terrain3DRegion *p_region, const int p_mesh_id);
|
||||
void _set_mmi_lod_ranges(MultiMeshInstance3D *p_mmi, const Ref<Terrain3DMeshAsset> &p_ma, const int p_lod);
|
||||
void _rebuild_ma_meshes();
|
||||
void _set_mmi_lod_ranges(RID p_mmi, const Ref<Terrain3DMeshAsset> &p_ma, const int p_lod);
|
||||
void _update_vertex_spacing(const real_t p_vertex_spacing);
|
||||
void _destroy_mmi_by_mesh(const int p_mesh_id);
|
||||
void _destroy_mmi_by_location(const Vector2i &p_region_loc, const int p_mesh_id);
|
||||
void _destroy_mmi_by_cell(const Vector2i &p_region_loc, const int p_mesh_id, const Vector2i p_cell, const int p_lod = INT32_MAX);
|
||||
void _backup_region(const Ref<Terrain3DRegion> &p_region);
|
||||
Ref<MultiMesh> _create_multimesh(const int p_mesh_id, const int p_lod, const TypedArray<Transform3D> &p_xforms = TypedArray<Transform3D>(), const PackedColorArray &p_colors = PackedColorArray()) const;
|
||||
RID _create_multimesh(const int p_mesh_id, const int p_lod, const TypedArray<Transform3D> &p_xforms = TypedArray<Transform3D>(), const PackedColorArray &p_colors = PackedColorArray()) const;
|
||||
Vector2i _get_cell(const Vector3 &p_global_position, const int p_region_size) const;
|
||||
Array _get_usable_height(const Vector3 &p_global_position, const Vector2 &p_slope_range, const bool p_on_collision, const real_t p_raycast_start) const;
|
||||
|
||||
|
|
@ -88,7 +87,8 @@ public:
|
|||
void update_mmis(const int p_mesh_id = -1, const Vector2i &p_region_loc = V2I_MAX, const bool p_rebuild = false);
|
||||
|
||||
void reset_density_counter() { _density_counter = 0; }
|
||||
void dump_mmis();
|
||||
void set_show_instances(const bool p_visible);
|
||||
bool get_show_instances() const { return _show_instances; }
|
||||
|
||||
protected:
|
||||
static void _bind_methods();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
#include <godot_cpp/classes/standard_material3d.hpp>
|
||||
|
||||
#include "logger.h"
|
||||
#include "terrain_3d_instancer.h"
|
||||
#include "terrain_3d_mesh_asset.h"
|
||||
|
||||
///////////////////////////
|
||||
|
|
@ -134,6 +133,7 @@ void Terrain3DMeshAsset::clear() {
|
|||
_height_offset = 0.f;
|
||||
_density = 10.f;
|
||||
_cast_shadows = SHADOWS_ON;
|
||||
_visibility_layers = 1;
|
||||
_material_override.unref();
|
||||
_material_overlay.unref();
|
||||
_last_lod = MAX_LOD_COUNT - 1;
|
||||
|
|
@ -217,6 +217,14 @@ void Terrain3DMeshAsset::set_instance_count(const uint32_t p_amount) {
|
|||
|
||||
void Terrain3DMeshAsset::set_scene_file(const Ref<PackedScene> &p_scene_file) {
|
||||
SET_IF_DIFF(_packed_scene, p_scene_file);
|
||||
scene_file_changed = true;
|
||||
LOG(INFO, "Setting scene file: ", p_scene_file);
|
||||
LOG(DEBUG, "Emitting instancer_setting_changed, ID: ", _id);
|
||||
emit_signal("instancer_setting_changed", _id);
|
||||
}
|
||||
|
||||
void Terrain3DMeshAsset::parse_scene_meshes() {
|
||||
LOG(INFO, " parsing scene file and storing meshes");
|
||||
_meshes.clear();
|
||||
if (_packed_scene.is_valid()) {
|
||||
Node *node = _packed_scene->instantiate();
|
||||
|
|
@ -225,7 +233,7 @@ void Terrain3DMeshAsset::set_scene_file(const Ref<PackedScene> &p_scene_file) {
|
|||
_packed_scene.unref();
|
||||
return;
|
||||
}
|
||||
LOG(INFO, "ID ", _id, ", ", _name, ": Setting scene file and instantiating node: ", p_scene_file);
|
||||
LOG(INFO, "ID ", _id, ", ", _name, ": Instantiating node: ", _packed_scene->get_name());
|
||||
_generated_type = TYPE_NONE;
|
||||
_height_offset = 0.0f;
|
||||
_material_override.unref();
|
||||
|
|
@ -271,6 +279,10 @@ void Terrain3DMeshAsset::set_scene_file(const Ref<PackedScene> &p_scene_file) {
|
|||
}
|
||||
// Duplicate the mesh to make each Terrain3DMeshAsset unique
|
||||
Ref<Mesh> mesh = mi->get_mesh()->duplicate();
|
||||
if (mesh.is_null()) {
|
||||
LOG(WARN, "MeshInstance3D ", mi->get_name(), " has no mesh, skipping");
|
||||
continue;
|
||||
}
|
||||
// Apply the active material from the scene to the mesh, including MI or Geom overrides
|
||||
for (int j = 0; j < mi->get_surface_override_material_count(); j++) {
|
||||
Ref<Material> mat = mi->get_active_material(j);
|
||||
|
|
@ -282,6 +294,10 @@ void Terrain3DMeshAsset::set_scene_file(const Ref<PackedScene> &p_scene_file) {
|
|||
}
|
||||
if (_meshes.size() > 0) {
|
||||
Ref<Mesh> mesh = _meshes[0];
|
||||
if (mesh.is_null()) {
|
||||
LOG(ERROR, "First mesh is null after loading scene");
|
||||
return;
|
||||
}
|
||||
_density = CLAMP(10.f / mesh->get_aabb().get_volume(), 0.01f, 10.0f);
|
||||
} else {
|
||||
set_generated_type(TYPE_TEXTURE_CARD);
|
||||
|
|
@ -404,6 +420,13 @@ ShadowCasting Terrain3DMeshAsset::get_lod_cast_shadows(const int p_lod_id) const
|
|||
return _cast_shadows;
|
||||
}
|
||||
|
||||
inline void Terrain3DMeshAsset::set_visibility_layers(const uint32_t p_layers) {
|
||||
SET_IF_DIFF(_visibility_layers, p_layers);
|
||||
LOG(INFO, _name, ": Setting visibility layers: ", p_layers);
|
||||
LOG(DEBUG, "Emitting instancer_setting_changed, ID: ", _id);
|
||||
emit_signal("instancer_setting_changed", _id);
|
||||
}
|
||||
|
||||
void Terrain3DMeshAsset::set_material_override(const Ref<Material> &p_material) {
|
||||
SET_IF_DIFF(_material_override, p_material);
|
||||
LOG(INFO, "ID ", _id, ", ", _name, ": Setting material override: ", p_material);
|
||||
|
|
@ -521,6 +544,10 @@ void Terrain3DMeshAsset::set_fade_margin(const real_t p_fade_margin) {
|
|||
emit_signal("instancer_setting_changed", _id);
|
||||
}
|
||||
|
||||
void Terrain3DMeshAsset::set_scene_file_changed(const bool p_changed) {
|
||||
SET_IF_DIFF(scene_file_changed, p_changed);
|
||||
}
|
||||
|
||||
///////////////////////////
|
||||
// Protected Functions
|
||||
///////////////////////////
|
||||
|
|
@ -592,6 +619,8 @@ void Terrain3DMeshAsset::_bind_methods() {
|
|||
ClassDB::bind_method(D_METHOD("get_density"), &Terrain3DMeshAsset::get_density);
|
||||
ClassDB::bind_method(D_METHOD("set_cast_shadows", "mode"), &Terrain3DMeshAsset::set_cast_shadows);
|
||||
ClassDB::bind_method(D_METHOD("get_cast_shadows"), &Terrain3DMeshAsset::get_cast_shadows);
|
||||
ClassDB::bind_method(D_METHOD("set_visibility_layers", "layers"), &Terrain3DMeshAsset::set_visibility_layers);
|
||||
ClassDB::bind_method(D_METHOD("get_visibility_layers"), &Terrain3DMeshAsset::get_visibility_layers);
|
||||
ClassDB::bind_method(D_METHOD("set_material_override", "material"), &Terrain3DMeshAsset::set_material_override);
|
||||
ClassDB::bind_method(D_METHOD("get_material_override"), &Terrain3DMeshAsset::get_material_override);
|
||||
ClassDB::bind_method(D_METHOD("set_material_overlay", "material"), &Terrain3DMeshAsset::set_material_overlay);
|
||||
|
|
@ -645,6 +674,7 @@ void Terrain3DMeshAsset::_bind_methods() {
|
|||
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "height_offset", PROPERTY_HINT_RANGE, "-20.0,20.0,.005"), "set_height_offset", "get_height_offset");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "density", PROPERTY_HINT_RANGE, ".01,10.0,.005"), "set_density", "get_density");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "cast_shadows", PROPERTY_HINT_ENUM, "Off,On,Double-Sided,Shadows Only"), "set_cast_shadows", "get_cast_shadows");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "visibility_layers", PROPERTY_HINT_LAYERS_3D_RENDER), "set_visibility_layers", "get_visibility_layers");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "material_override", PROPERTY_HINT_RESOURCE_TYPE, "BaseMaterial3D,ShaderMaterial"), "set_material_override", "get_material_override");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "material_overlay", PROPERTY_HINT_RESOURCE_TYPE, "BaseMaterial3D,ShaderMaterial"), "set_material_overlay", "get_material_overlay");
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
#define TERRAIN3D_MESH_ASSET_CLASS_H
|
||||
|
||||
#include <godot_cpp/classes/array_mesh.hpp>
|
||||
#include <godot_cpp/classes/geometry_instance3d.hpp>
|
||||
#include <godot_cpp/classes/material.hpp>
|
||||
#include <godot_cpp/classes/packed_scene.hpp>
|
||||
#include <godot_cpp/classes/resource.hpp>
|
||||
|
|
@ -13,10 +12,10 @@
|
|||
#include "constants.h"
|
||||
#include "terrain_3d_asset_resource.h"
|
||||
|
||||
using ShadowCasting = GeometryInstance3D::ShadowCastingSetting;
|
||||
constexpr ShadowCasting SHADOWS_ON = GeometryInstance3D::SHADOW_CASTING_SETTING_ON;
|
||||
constexpr ShadowCasting SHADOWS_OFF = GeometryInstance3D::SHADOW_CASTING_SETTING_OFF;
|
||||
constexpr ShadowCasting SHADOWS_ONLY = GeometryInstance3D::SHADOW_CASTING_SETTING_SHADOWS_ONLY;
|
||||
using ShadowCasting = RenderingServer::ShadowCastingSetting;
|
||||
constexpr ShadowCasting SHADOWS_ON = RenderingServer::SHADOW_CASTING_SETTING_ON;
|
||||
constexpr ShadowCasting SHADOWS_OFF = RenderingServer::SHADOW_CASTING_SETTING_OFF;
|
||||
constexpr ShadowCasting SHADOWS_ONLY = RenderingServer::SHADOW_CASTING_SETTING_SHADOWS_ONLY;
|
||||
|
||||
class Terrain3DMeshAsset : public Terrain3DAssetResource {
|
||||
GDCLASS(Terrain3DMeshAsset, Terrain3DAssetResource);
|
||||
|
|
@ -42,6 +41,7 @@ private:
|
|||
real_t _height_offset = 0.f;
|
||||
real_t _density = 10.f;
|
||||
ShadowCasting _cast_shadows = SHADOWS_ON;
|
||||
uint32_t _visibility_layers = 1;
|
||||
Ref<Material> _material_override;
|
||||
Ref<Material> _material_overlay;
|
||||
int _last_lod = MAX_LOD_COUNT - 1;
|
||||
|
|
@ -59,6 +59,7 @@ private:
|
|||
static bool _sort_lod_nodes(const Node *a, const Node *b);
|
||||
Ref<ArrayMesh> _create_generated_mesh(const GenType p_type = TYPE_TEXTURE_CARD) const;
|
||||
Ref<Material> _get_material();
|
||||
bool scene_file_changed = false;
|
||||
|
||||
public:
|
||||
Terrain3DMeshAsset() { clear(); }
|
||||
|
|
@ -83,6 +84,7 @@ public:
|
|||
uint32_t get_instance_count() const { return _instance_count; }
|
||||
|
||||
void set_scene_file(const Ref<PackedScene> &p_scene_file);
|
||||
void parse_scene_meshes();
|
||||
Ref<PackedScene> get_scene_file() const { return _packed_scene; }
|
||||
void set_generated_type(const GenType p_type);
|
||||
GenType get_generated_type() const { return _generated_type; }
|
||||
|
|
@ -96,6 +98,8 @@ public:
|
|||
void set_cast_shadows(const ShadowCasting p_cast_shadows);
|
||||
ShadowCasting get_cast_shadows() const { return _cast_shadows; };
|
||||
ShadowCasting get_lod_cast_shadows(const int p_lod_id) const;
|
||||
void set_visibility_layers(const uint32_t p_layers);
|
||||
uint32_t get_visibility_layers() const { return _visibility_layers; }
|
||||
void set_material_override(const Ref<Material> &p_material);
|
||||
Ref<Material> get_material_override() const { return _material_override; }
|
||||
void set_material_overlay(const Ref<Material> &p_material);
|
||||
|
|
@ -140,6 +144,8 @@ public:
|
|||
real_t get_lod9_range() const { return _lod_ranges[9]; }
|
||||
void set_fade_margin(const real_t p_fade_margin);
|
||||
real_t get_fade_margin() const { return _fade_margin; };
|
||||
bool get_scene_file_changed() const { return scene_file_changed; }
|
||||
void set_scene_file_changed(const bool p_changed);
|
||||
|
||||
protected:
|
||||
void _validate_property(PropertyInfo &p_property) const;
|
||||
|
|
|
|||
Loading…
Reference in New Issue