Skip to content

Commit

Permalink
Add terrain occlusion baking to the editor
Browse files Browse the repository at this point in the history
  • Loading branch information
tcoxon authored and TokisanGames committed Nov 6, 2023
1 parent ac9fc8a commit 5dfaa13
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 1 deletion.
28 changes: 28 additions & 0 deletions project/addons/terrain_3d/editor/components/bake_dialog.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@tool
extends ConfirmationDialog

var lod: int = 0
var description: String = ""


func _ready() -> void:
set_unparent_when_invisible(true)
about_to_popup.connect(_on_about_to_popup)
visibility_changed.connect(_on_visibility_changed)
%LodBox.value_changed.connect(_on_lod_box_value_changed)


func _on_about_to_popup() -> void:
lod = %LodBox.value


func _on_visibility_changed() -> void:
# Change text on the autowrap label only when the popup is visible.
# Works around Godot issue #47005:
# https://github.com/godotengine/godot/issues/47005
if visible:
%DescriptionLabel.text = description


func _on_lod_box_value_changed(value) -> void:
lod = %LodBox.value
41 changes: 41 additions & 0 deletions project/addons/terrain_3d/editor/components/bake_dialog.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[gd_scene load_steps=2 format=3 uid="uid://bhvrrmb8bk1bt"]

[ext_resource type="Script" path="res://addons/terrain_3d/editor/components/bake_dialog.gd" id="1_57670"]

[node name="bake_dialog" type="ConfirmationDialog"]
title = "Bake Terrain3D Mesh"
position = Vector2i(0, 36)
size = Vector2i(400, 115)
visible = true
script = ExtResource("1_57670")

[node name="VBoxContainer" type="VBoxContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
grow_horizontal = 2
grow_vertical = 2

[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 20

[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "LOD:"

[node name="LodBox" type="SpinBox" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
max_value = 8.0
value = 4.0

[node name="DescriptionLabel" type="Label" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
autowrap_mode = 2
88 changes: 88 additions & 0 deletions project/addons/terrain_3d/editor/components/baker.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
extends HBoxContainer

const BakeDialog: PackedScene = preload("res://addons/terrain_3d/editor/components/bake_dialog.tscn")
const BAKE_MESH_DESCRIPTION: String = "This will create a child MeshInstance3D. LOD4+ is recommended. LOD0 is slow and dense with vertices every 1 unit. It is not an optimal mesh."
const BAKE_OCCLUDER_DESCRIPTION: String = "This will create a child OccluderInstance3D. LOD4+ is recommended and will take 5+ seconds per region to generate. LOD0 is unnecessarily dense and slow."

var plugin: EditorPlugin
var bake_method: Callable
var bake_dialog: ConfirmationDialog
var bake_mesh_btn: Button
var bake_occluder_btn: Button


func _enter_tree() -> void:
bake_dialog = BakeDialog.instantiate()
bake_dialog.hide()
bake_dialog.confirmed.connect(func(): bake_method.call())

bake_mesh_btn = Button.new()
bake_mesh_btn.text = "Bake ArrayMesh"
bake_mesh_btn.pressed.connect(_on_bake_mesh_btn_pressed)
add_child(bake_mesh_btn)

bake_occluder_btn = Button.new()
bake_occluder_btn.text = "Bake Occluder3D"
bake_occluder_btn.pressed.connect(_on_bake_occluder_btn_pressed)
add_child(bake_occluder_btn)


func _on_bake_mesh_btn_pressed() -> void:
if plugin.terrain:
bake_method = _bake_mesh
bake_dialog.description = BAKE_MESH_DESCRIPTION
plugin.get_editor_interface().popup_dialog_centered(bake_dialog)


func _bake_mesh() -> void:
var mesh: Mesh = plugin.terrain.bake_mesh(bake_dialog.lod, Terrain3DStorage.HEIGHT_FILTER_NEAREST)
if !mesh:
push_error("Failed to bake mesh from Terrain3D")
return
var undo := plugin.get_undo_redo()

var mesh_instance := MeshInstance3D.new()
mesh_instance.name = &"MeshInstance3D"
mesh_instance.mesh = mesh
mesh_instance.set_skeleton_path(NodePath())

undo.create_action("Terrain3D Bake ArrayMesh")
undo.add_do_method(plugin.terrain, &"add_child", mesh_instance, true)
undo.add_undo_method(plugin.terrain, &"remove_child", mesh_instance)
undo.add_do_property(mesh_instance, &"owner", plugin.terrain.owner)
undo.add_do_reference(mesh_instance)
undo.commit_action()


func _on_bake_occluder_btn_pressed() -> void:
if plugin.terrain:
bake_method = _bake_occluder
bake_dialog.description = BAKE_OCCLUDER_DESCRIPTION
plugin.get_editor_interface().popup_dialog_centered(bake_dialog)


func _bake_occluder() -> void:
var mesh: Mesh = plugin.terrain.bake_mesh(bake_dialog.lod, Terrain3DStorage.HEIGHT_FILTER_MINIMUM)
if !mesh:
push_error("Failed to bake mesh from Terrain3D")
return
assert(mesh.get_surface_count() == 1)

var undo := plugin.get_undo_redo()

var occluder := ArrayOccluder3D.new()
var arrays := mesh.surface_get_arrays(0)
assert(arrays.size() > Mesh.ARRAY_INDEX)
assert(arrays[Mesh.ARRAY_INDEX] != null)
occluder.set_arrays(arrays[Mesh.ARRAY_VERTEX], arrays[Mesh.ARRAY_INDEX])

var occluder_instance := OccluderInstance3D.new()
occluder_instance.name = &"OccluderInstance3D"
occluder_instance.occluder = occluder

undo.create_action("Terrain3D Bake Occluder3D")
undo.add_do_method(plugin.terrain, &"add_child", occluder_instance, true)
undo.add_undo_method(plugin.terrain, &"remove_child", occluder_instance)
undo.add_do_property(occluder_instance, &"owner", plugin.terrain.owner)
undo.add_do_reference(occluder_instance)
undo.commit_action()
9 changes: 9 additions & 0 deletions project/addons/terrain_3d/editor/components/ui.gd
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ extends Node
# Includes
const Toolbar: Script = preload("res://addons/terrain_3d/editor/components/toolbar.gd")
const ToolSettings: Script = preload("res://addons/terrain_3d/editor/components/tool_settings.gd")
const Baker: Script = preload("res://addons/terrain_3d/editor/components/baker.gd")
const RING1: String = "res://addons/terrain_3d/editor/brushes/ring1.exr"
const COLOR_RAISE := Color.WHITE
const COLOR_LOWER := Color.BLACK
Expand All @@ -23,6 +24,7 @@ const COLOR_PICK_ROUGH := Color.ROYAL_BLUE
var plugin: EditorPlugin # Actually Terrain3DEditorPlugin, but Godot still has CRC errors
var toolbar: Toolbar
var toolbar_settings: ToolSettings
var baker: Baker
var setting_has_changed: bool = false
var visible: bool = false
var picking: int = Terrain3DEditor.TOOL_MAX
Expand All @@ -43,8 +45,13 @@ func _enter_tree() -> void:
toolbar_settings.connect("picking", _on_picking)
toolbar_settings.hide()

baker = Baker.new()
baker.plugin = plugin
baker.hide()

plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, toolbar)
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, toolbar_settings)
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, baker)

decal = Decal.new()
add_child(decal)
Expand All @@ -61,13 +68,15 @@ func _exit_tree() -> void:
plugin.remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, toolbar_settings)
toolbar.queue_free()
toolbar_settings.queue_free()
baker.queue_free()
decal.queue_free()
decal_timer.queue_free()


func set_visible(p_visible: bool) -> void:
visible = p_visible
toolbar.set_visible(p_visible)
baker.set_visible(p_visible)

if p_visible:
p_visible = plugin.editor.get_tool() != Terrain3DEditor.REGION
Expand Down
4 changes: 4 additions & 0 deletions project/project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,7 @@ sprint={

3d_physics/layer_1="Environment"
3d_physics/layer_2="Player"

[rendering]

occlusion_culling/use_occlusion_culling=true
60 changes: 60 additions & 0 deletions src/terrain_3d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/height_map_shape3d.hpp>
#include <godot_cpp/classes/rendering_server.hpp>
#include <godot_cpp/classes/surface_tool.hpp>
#include <godot_cpp/classes/time.hpp>
#include <godot_cpp/classes/v_box_container.hpp> // for get_editor_main_screen()
#include <godot_cpp/classes/viewport.hpp>
Expand Down Expand Up @@ -753,6 +754,64 @@ Vector3 Terrain3D::get_intersection(Vector3 p_position, Vector3 p_direction) {
return Vector3(__FLT_MAX__, __FLT_MAX__, __FLT_MAX__);
}

/**
* Generates a static ArrayMesh for the terrain.
* p_lod (0-8): Determines the granularity of the generated mesh.
* p_filter: Controls how vertices' Y coordinates are generated from the height map.
* HEIGHT_FILTER_NEAREST: Samples the height map in a 'nearest neighbour' fashion.
* HEIGHT_FILTER_MINIMUM: Samples a range of heights around each vertex and returns the lowest.
* This takes longer than ..._NEAREST, but can be used to create occluders, since it can guarantee the
* generated mesh will not extend above or outside the clipmap at any LOD.
*/
Ref<Mesh> Terrain3D::bake_mesh(int p_lod, Terrain3DStorage::HeightFilter p_filter) const {
LOG(INFO, "Baking mesh at lod: ", p_lod, " with filter: ", p_filter);
Ref<Mesh> result;
ERR_FAIL_COND_V(!_storage.is_valid(), result);

int32_t region_size = (int)_storage->get_region_size();
int32_t step = 1 << CLAMP(p_lod, 0, 8);

Ref<SurfaceTool> st;
st.instantiate();
st->begin(Mesh::PRIMITIVE_TRIANGLES);

TypedArray<Vector2i> region_offsets = _storage->get_region_offsets();
for (int r = 0; r < region_offsets.size(); ++r) {
Vector2i region_offset = (Vector2i)region_offsets[r] * region_size;

for (int32_t z = region_offset.y; z < region_offset.y + region_size; z += step) {
for (int32_t x = region_offset.x; x < region_offset.x + region_size; x += step) {
Vector3 v1 = _storage->get_mesh_vertex(p_lod, p_filter, Vector3(x, 0.0, z));
Vector3 v2 = _storage->get_mesh_vertex(p_lod, p_filter, Vector3(x + step, 0.0, z + step));
Vector3 v3 = _storage->get_mesh_vertex(p_lod, p_filter, Vector3(x, 0.0, z + step));
st->set_uv(Vector2(v1.x, v1.z));
st->add_vertex(v1);
st->set_uv(Vector2(v2.x, v2.z));
st->add_vertex(v2);
st->set_uv(Vector2(v3.x, v3.z));
st->add_vertex(v3);

v1 = _storage->get_mesh_vertex(p_lod, p_filter, Vector3(x, 0.0, z));
v2 = _storage->get_mesh_vertex(p_lod, p_filter, Vector3(x + step, 0.0, z));
v3 = _storage->get_mesh_vertex(p_lod, p_filter, Vector3(x + step, 0.0, z + step));
st->set_uv(Vector2(v1.x, v1.z));
st->add_vertex(v1);
st->set_uv(Vector2(v2.x, v2.z));
st->add_vertex(v2);
st->set_uv(Vector2(v3.x, v3.z));
st->add_vertex(v3);
}
}
}

st->index();
st->generate_normals();
st->generate_tangents();
st->optimize_indices_for_cache();
result = st->commit();
return result;
}

///////////////////////////
// Protected Functions
///////////////////////////
Expand Down Expand Up @@ -886,6 +945,7 @@ void Terrain3D::_bind_methods() {
// Expose 'update_aabbs' so it can be used in Callable. Not ideal.
ClassDB::bind_method(D_METHOD("update_aabbs"), &Terrain3D::update_aabbs);
ClassDB::bind_method(D_METHOD("get_intersection", "position", "direction"), &Terrain3D::get_intersection);
ClassDB::bind_method(D_METHOD("bake_mesh", "lod", "filter"), &Terrain3D::bake_mesh);

ADD_PROPERTY(PropertyInfo(Variant::STRING, "version", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY), "", "get_version");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "storage", PROPERTY_HINT_RESOURCE_TYPE, "Terrain3DStorage"), "set_storage", "get_storage");
Expand Down
6 changes: 5 additions & 1 deletion src/terrain_3d.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <godot_cpp/classes/camera3d.hpp>
#include <godot_cpp/classes/editor_plugin.hpp>
#include <godot_cpp/classes/geometry_instance3d.hpp>
#include <godot_cpp/classes/mesh.hpp>
#include <godot_cpp/classes/static_body3d.hpp>

#include "terrain_3d_material.h"
Expand Down Expand Up @@ -135,9 +136,12 @@ class Terrain3D : public Node3D {
void update_aabbs();
Vector3 get_intersection(Vector3 p_position, Vector3 p_direction);

// Baking methods
Ref<Mesh> bake_mesh(int p_lod, Terrain3DStorage::HeightFilter p_filter = Terrain3DStorage::HEIGHT_FILTER_NEAREST) const;

protected:
void _notification(int p_what);
static void _bind_methods();
};

#endif // TERRAIN3D_CLASS_H
#endif // TERRAIN3D_CLASS_H
35 changes: 35 additions & 0 deletions src/terrain_3d_storage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,37 @@ Color Terrain3DStorage::get_color(Vector3 p_global_position) {
return clr;
}

/**
* Returns the location of a terrain vertex at a certain LOD.
* p_lod (0-8): Determines how many heights around the given global position will be sampled.
* p_filter:
* HEIGHT_FILTER_NEAREST: Samples the height map at the exact coordinates given.
* HEIGHT_FILTER_MINIMUM: Samples (1 << p_lod) ** 2 heights around the given coordinates and returns the lowest.
* p_global_position: X and Z coordinates of the vertex. Heights will be sampled around these coordinates.
*/
Vector3 Terrain3DStorage::get_mesh_vertex(int32_t p_lod, HeightFilter p_filter, Vector3 p_global_position) {
LOG(INFO, "Calculating vertex location");
int32_t step = 1 << CLAMP(p_lod, 0, 8);
real_t height = 0.0;
switch (p_filter) {
case HEIGHT_FILTER_NEAREST: {
height = get_height(p_global_position);
} break;
case HEIGHT_FILTER_MINIMUM: {
height = get_height(p_global_position);
for (int32_t dx = -step / 2; dx < step / 2; dx += 1) {
for (int32_t dz = -step / 2; dz < step / 2; dz += 1) {
real_t h = get_height(p_global_position + Vector3(dx, 0.0, dz));
if (h < height) {
height = h;
}
}
}
} break;
}
return Vector3(p_global_position.x, height, p_global_position.z);
}

/**
* Returns sanitized maps of either a region set or a uniform set
* Verifies size, vailidity, and format of maps
Expand Down Expand Up @@ -937,6 +968,9 @@ void Terrain3DStorage::_bind_methods() {
BIND_ENUM_CONSTANT(SIZE_1024);
//BIND_ENUM_CONSTANT(SIZE_2048);

BIND_ENUM_CONSTANT(HEIGHT_FILTER_NEAREST);
BIND_ENUM_CONSTANT(HEIGHT_FILTER_MINIMUM);

BIND_CONSTANT(REGION_MAP_SIZE);

ClassDB::bind_method(D_METHOD("set_version", "version"), &Terrain3DStorage::set_version);
Expand Down Expand Up @@ -975,6 +1009,7 @@ void Terrain3DStorage::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_color", "global_position"), &Terrain3DStorage::get_color);
ClassDB::bind_method(D_METHOD("get_control", "global_position"), &Terrain3DStorage::get_control);
ClassDB::bind_method(D_METHOD("get_roughness", "global_position"), &Terrain3DStorage::get_roughness);
ClassDB::bind_method(D_METHOD("get_mesh_vertex", "lod", "filter", "global_position"), &Terrain3DStorage::get_mesh_vertex);
ClassDB::bind_method(D_METHOD("force_update_maps", "map_type"), &Terrain3DStorage::force_update_maps, DEFVAL(TYPE_MAX));

ClassDB::bind_static_method("Terrain3DStorage", D_METHOD("load_image", "file_name", "cache_mode", "r16_height_range", "r16_size"), &Terrain3DStorage::load_image, DEFVAL(ResourceLoader::CACHE_MODE_IGNORE), DEFVAL(Vector2(0, 255)), DEFVAL(Vector2i(0, 0)));
Expand Down
Loading

0 comments on commit 5dfaa13

Please sign in to comment.