Error log spam when using HLOD

Godot Version

Godot v4.4.stable - Debian GNU/Linux trixie/sid trixie on Wayland - X11 display driver, Multi-window, 1 monitor - Vulkan (Forward+) - integrated Intel(R) Iris(R) Xe Graphics (RPL-P) - 13th Gen Intel(R) Core™ i7-1360P (16 threads)

Question

I’m trying to set up HLOD (hierarchical level of detail) for my game’s characters.

However, I’m getting this error message printed twice per character per frame from what I can tell:

ERROR: Condition "dep_instance->array_index == -1" is true. Continuing.
   at: instance_set_base (servers/rendering/renderer_scene_cull.cpp:708)

The character setup code (runs once per character) looks like this:

void MMPawn::_init_scene() {
	if (_scene_root) {
		remove_child(_scene_root);
		_scene_root->queue_free();
		_scene_root = nullptr;
		_skeleton = nullptr;
		_head = nullptr;
		_body = nullptr;
		_head_glow = nullptr;
		_body_glow = nullptr;
		_cheap = nullptr;
		_cheap_glow = nullptr;
		_anim = nullptr;
		_fpcamlook = nullptr;
		_topcamlook = nullptr;
		_found_gimbal = false;
	}

	ERR_FAIL_COND(_def.is_null());
	ERR_FAIL_COND(_def->get_model_scene().is_null());

	Ref<CapsuleShape3D> capsule;
	capsule.instantiate();
	capsule->set_radius(_def->get_radius());
	capsule->set_height(_def->get_height());
	_shape->set_position(Vector3(0.0f, 0.5f * _def->get_height(), 0.0f));
	_shape->set_shape(capsule);

	_agent->set_radius(_def->get_radius());
	_agent->set_height(_def->get_height());
	_agent->set_max_speed(_def->get_default_speed());

	set_physics_material_override(_def->get_physics_material());
	set_motion_mode(_def->get_motion_mode());

	_fpcam->set_position(Vector3(0, _def->get_eye_height_offset(), 0));

	_scene_root = Object::cast_to<Node3D>(_def->get_model_scene()->instantiate());
#ifdef DEBUG_ENABLED
	if (unlikely(Engine::get_singleton()->is_editor_hint())) {
		Node *old_scene_root = get_node_or_null(NodePath(_scene_root->get_name()));
		if (old_scene_root) {
			// scene gets duplicated on hot reload
			remove_child(old_scene_root);
			old_scene_root->queue_free();
		}
	}
#endif
	add_child(_scene_root, false, INTERNAL_MODE_BACK);
	_scene_root->set_owner(this);

	_skeleton = _scene_root->get_node<Skeleton3D>(SKELETON_3D);
	_head = _skeleton->get_node<MeshInstance3D>(HEAD_MESH);
	_head->set_layer_mask(RENDER_3D_LAYER_PAWNS_MASK);
	_head->set_cast_shadows_setting(_player_controlled ? GeometryInstance3D::SHADOW_CASTING_SETTING_SHADOWS_ONLY : GeometryInstance3D::SHADOW_CASTING_SETTING_ON);

	_head_glow = Object::cast_to<MeshInstance3D>(_head->duplicate(0));
	_head_glow->set_name("head-mesh-glow");
	_head_glow->set_layer_mask(RENDER_3D_LAYER_GLOW_MASK);
	_head_glow->set_cast_shadows_setting(GeometryInstance3D::SHADOW_CASTING_SETTING_ON);

	_skeleton->add_child(_head_glow);

	_body = _skeleton->get_node<MeshInstance3D>(BODY_MESH);
	_body->set_layer_mask(RENDER_3D_LAYER_PAWNS_MASK);

	_body_glow = Object::cast_to<MeshInstance3D>(_body->duplicate(0));
	_body_glow->set_name("body-mesh-glow");
	_body_glow->set_layer_mask(RENDER_3D_LAYER_GLOW_MASK);

	_skeleton->add_child(_body_glow);

	_cheap = memnew(MeshInstance3D);
	_cheap->set_name("cheap-mesh");
	_cheap->set_layer_mask(RENDER_3D_LAYER_PAWNS_MASK);
	_cheap->set_gi_mode(GeometryInstance3D::GI_MODE_DISABLED);
	_cheap->set_lod_bias(CHEAP_LOD_BIAS);
	_cheap->set_transform(_skeleton->get_transform());
	_scene_root->add_child(_cheap, false, INTERNAL_MODE_BACK);

	_cheap_glow = memnew(MeshInstance3D);
	_cheap_glow->set_name("cheap-mesh-glow");
	_cheap_glow->set_layer_mask(RENDER_3D_LAYER_GLOW_MASK);
	_cheap_glow->set_gi_mode(GeometryInstance3D::GI_MODE_DISABLED);
	_cheap_glow->set_lod_bias(CHEAP_LOD_BIAS);
	_cheap_glow->set_transform(_skeleton->get_transform());
	_scene_root->add_child(_cheap_glow, false, INTERNAL_MODE_BACK);

	_on_set_material_overlay();

	_anim = _scene_root->get_node<AnimationPlayer>(ANIMATION_PLAYER);
	_anim->set_auto_capture(true);

	_fpcamlook = memnew(MMFirstPersonCameraSkeletonModifier);
	_fpcamlook->set_camera(_fpcam);
	_fpcamlook->set_active(false);
	_skeleton->add_child(_fpcamlook, true);

	_topcamlook = memnew(LookAtModifier3D);
	_topcamlook->set_duration(0.2f);
	_topcamlook->set_use_angle_limitation(true);
	_topcamlook->set_primary_limit_angle(Math::deg_to_rad(120.0f));
	_topcamlook->set_secondary_limit_angle(Math::deg_to_rad(120.0f));
	_topcamlook->set_influence(0.0f);
	_skeleton->add_child(_topcamlook, true);
	_found_gimbal = false;

	_anim->play(IDLE);
	Ref<Mesh> idle_mesh = _def->get_cheap_mesh_for_animation(IDLE);
	_cheap->set_mesh(idle_mesh);
	_cheap_glow->set_mesh(idle_mesh);

	_head->set_visibility_range_fade_mode(GeometryInstance3D::VISIBILITY_RANGE_FADE_DISABLED);
	_body->set_visibility_range_fade_mode(GeometryInstance3D::VISIBILITY_RANGE_FADE_DISABLED);
	_cheap->set_visibility_range_fade_mode(GeometryInstance3D::VISIBILITY_RANGE_FADE_DISABLED);
	_head_glow->set_visibility_range_fade_mode(GeometryInstance3D::VISIBILITY_RANGE_FADE_DISABLED);
	_body_glow->set_visibility_range_fade_mode(GeometryInstance3D::VISIBILITY_RANGE_FADE_DISABLED);
	_cheap_glow->set_visibility_range_fade_mode(GeometryInstance3D::VISIBILITY_RANGE_FADE_DISABLED);
	_head->set_visibility_parent(_head->get_path_to(_cheap));
	_body->set_visibility_parent(_body->get_path_to(_cheap));
	_cheap->set_visibility_range_begin(CHEAP_LOD_RANGE);
	_cheap->set_visibility_range_begin_margin(CHEAP_LOD_MARGIN);
	_head_glow->set_visibility_parent(_head_glow->get_path_to(_cheap_glow));
	_body_glow->set_visibility_parent(_body_glow->get_path_to(_cheap_glow));
	_cheap_glow->set_visibility_range_begin(CHEAP_LOD_RANGE);
	_cheap_glow->set_visibility_range_begin_margin(CHEAP_LOD_MARGIN);

	if (likely(_world)) {
		_update_fade(_world->get_static_geometry_fade_y());
	}
}

CHEAP_LOD_RANGE is 5.0 and CHEAP_LOD_MARGIN is 1.0 in this test in case that makes a difference.

get_cheap_mesh_for_animation bakes and caches a static mesh with LODs of the first frame of any given animation, which allows the HLOD versions of the characters to be batched by the renderer:

Ref<Mesh> MMPawnDef::get_cheap_mesh_for_animation(const StringName &animation) {
	if (_cheap_mesh_for_animation.has(animation)) {
		return _cheap_mesh_for_animation.get(animation);
	}

	Node3D *scene = Object::cast_to<Node3D>(_model_scene->instantiate());
	ERR_FAIL_NULL_V(scene, Ref<Mesh>());
	SceneTree *tree = Object::cast_to<SceneTree>(Engine::get_singleton()->get_main_loop());
	tree->get_root()->add_child(scene);

	AnimationPlayer *anim = scene->get_node<AnimationPlayer>(ANIMATION_PLAYER);
	Skeleton3D *skeleton = scene->get_node<Skeleton3D>(SKELETON_3D);
	MeshInstance3D *head = skeleton->get_node<MeshInstance3D>(HEAD_MESH);
	MeshInstance3D *body = skeleton->get_node<MeshInstance3D>(BODY_MESH);

	anim->play(animation);
	anim->advance(0.0);
	skeleton->notification(Skeleton3D::NOTIFICATION_UPDATE_SKELETON);

	Ref<ArrayMesh> original_head = head->get_mesh();
	Ref<ArrayMesh> original_body = body->get_mesh();

	Ref<ArrayMesh> baked_head = head->bake_mesh_from_current_skeleton_pose();
	if (unlikely(baked_head.is_null())) {
		baked_head = original_head;
	}
	Ref<ArrayMesh> baked_body = body->bake_mesh_from_current_skeleton_pose();
	if (unlikely(baked_body.is_null())) {
		baked_body = original_body;
	}

	tree->get_root()->remove_child(scene);
	scene->queue_free();

	int32_t head_surface_count = baked_head->get_surface_count();
	int32_t body_surface_count = baked_body->get_surface_count();
	HashSet<Ref<Material>> seen_material;

	Ref<ImporterMesh> importer_mesh;
	importer_mesh.instantiate();

	for (int32_t i = 0; i < head_surface_count; i++) {
		Ref<Material> mat = original_head->surface_get_material(i);
		if (seen_material.has(mat)) {
			continue;
		}

		seen_material.insert(mat);

		Ref<SurfaceTool> st;
		st.instantiate();
		st->begin(Mesh::PRIMITIVE_TRIANGLES);
		st->set_material(mat);
		st->append_from(baked_head, i, head->get_transform());

		for (int32_t j = i + 1; j < head_surface_count; j++) {
			if (original_head->surface_get_material(j) == mat) {
				st->append_from(baked_head, j, head->get_transform());
			}
		}
		for (int32_t j = 0; j < body_surface_count; j++) {
			if (original_body->surface_get_material(j) == mat) {
				st->append_from(baked_body, j, body->get_transform());
			}
		}

		st->index();
		st->optimize_indices_for_cache();

		importer_mesh->add_surface(Mesh::PRIMITIVE_TRIANGLES, st->commit_to_arrays(), TypedArray<Array>(), Dictionary(), mat, original_head->surface_get_name(i));
	}

	for (int32_t i = 0; i < body_surface_count; i++) {
		Ref<Material> mat = original_body->surface_get_material(i);
		if (seen_material.has(mat)) {
			continue;
		}

		seen_material.insert(mat);

		Ref<SurfaceTool> st;
		st.instantiate();
		st->begin(Mesh::PRIMITIVE_TRIANGLES);
		st->set_material(mat);
		st->append_from(baked_body, i, body->get_transform());

		for (int32_t j = i + 1; j < body_surface_count; j++) {
			if (original_body->surface_get_material(j) == mat) {
				st->append_from(baked_body, j, body->get_transform());
			}
		}

		st->index();
		st->optimize_indices_for_cache();

		importer_mesh->add_surface(Mesh::PRIMITIVE_TRIANGLES, st->commit_to_arrays(), TypedArray<Array>(), Dictionary(), mat, original_body->surface_get_name(i));
	}

	importer_mesh->generate_lods(60.0f, 0.0f, Array());
	// TODO: https://github.com/godotengine/godot-proposals/issues/8567
	//importer_mesh->create_shadow_mesh();

	Ref<ArrayMesh> mesh = importer_mesh->get_mesh();
	_cheap_mesh_for_animation.insert(animation, mesh);

	return mesh;
}

Then when the NavigationAgent3D finishes its avoidance calculation, this code runs (MMPawn extends CharacterBody3D):

void MMPawn::_move_pawn(Vector3 velocity) {
	if (likely(PHYSICS_MOVE) || !is_on_floor()) {
		set_velocity(velocity + _accumulated_gravity);

		move_and_slide();

		int32_t collisions = get_slide_collision_count();
		for (int32_t i = 0; i < collisions; i++) {
			Ref<KinematicCollision3D> collision = get_slide_collision(i);
			RigidBody3D *rigid = Object::cast_to<RigidBody3D>(collision->get_collider());
			if (rigid) {
				rigid->apply_force(-collision->get_normal() * velocity.length() * _def->get_push_strength(), collision->get_position() - rigid->get_global_position());
			}
		}
	} else {
		translate(get_viewport()->get_physics_process_delta_time() * (velocity + _accumulated_gravity));
	}

	StringName animation;
	if (!is_on_floor()) {
		animation = FALL;
	} else if (velocity.is_zero_approx()) {
		animation = IDLE;
	} else {
		animation = WALK;
	}

	if (likely(_cheap)) {
		Ref<Mesh> mesh = _def->get_cheap_mesh_for_animation(animation);
		_cheap->set_mesh(mesh); // error printed here
		_cheap_glow->set_mesh(mesh); // error printed here
	}
	if (likely(_anim)) {
		_anim->play(animation);
	}
}

A character has a node tree that looks like this:

The HLOD does work, so characters that are closer to the camera are animated and characters that are further away are batched and not animated. However, the error message spam (600 error messages per physics frame in this test that spawns 300 characters) is costing a lot of performance and I can’t figure out what specifically is causing it.

1 Like