Skip to content

Commit

Permalink
Optimized HTerrainDetailLayer process function.
Browse files Browse the repository at this point in the history
- Update chunks only when the camera crosses chunk boundaries
- Spread calculations over time
  • Loading branch information
Zylann committed Jul 17, 2024
1 parent 9b0d219 commit 8c1b660
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 62 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ For a more detailed list of past and incoming changes, see the commit history.
- Exposed specular parameter in ground shaders
- Save terrain data immediately when assigning a new `data_directory` (attempt to workaround random saving issue #232)
- Updated name detection in the texture import dialog to work better with AmbientCG and PolyHaven (thanks to GustJc)
- Optimized `HTerrainDetailLayer` process function by updating chunks when needed and spreading calculations over time
- Fixed brush cursor looked off-center at close range when using tiny brush sizes
- Fixed error when trying to change `TextureSet` mode while it has textures in it
- Fixed shader parameters not displaying the right value in the inspector until they are set by the user at least once
Expand Down
157 changes: 95 additions & 62 deletions addons/zylann.hterrain/hterrain_detail_layer.gd
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ const _API_SHADER_PARAMS = {
set(mask):
render_layers = mask
for k in _chunks:
var chunk = _chunks[k]
chunk.set_layer_mask(mask)
var chunk : Chunk = _chunks[k]
chunk.mmi.set_layer_mask(mask)


# Exposes shadow casting setting.
Expand All @@ -156,14 +156,19 @@ const _API_SHADER_PARAMS = {
return
cast_shadow = option
for k in _chunks:
var mmi : HT_DirectMultiMeshInstance = _chunks[k]
mmi.set_cast_shadow(option)
var chunk : Chunk = _chunks[k]
chunk.mmi.set_cast_shadow(option)


class Chunk:
var mmi: HT_DirectMultiMeshInstance
var pending_update: bool = false


var _material: ShaderMaterial = null
var _default_shader: Shader = null

# Vector2 => HT_DirectMultiMeshInstance
# Vector2i => Chunk
var _chunks := {}

var _multimesh: MultiMesh
Expand All @@ -174,6 +179,9 @@ var _ambient_wind_time := 0.0
var _debug_wirecube_mesh: Mesh = null
var _debug_cubes := []
var _logger := HT_Logger.get_for(self)
var _prev_frame_cmin := Vector3i()
var _prev_frame_cmax := Vector3i()
var _pending_exit_updates : Array[Vector2i] = []


func _init():
Expand Down Expand Up @@ -402,14 +410,14 @@ func _notification(what: int):

func _set_visible(v: bool):
for k in _chunks:
var chunk : HT_DirectMultiMeshInstance = _chunks[k]
chunk.set_visible(v)
var chunk : Chunk = _chunks[k]
chunk.mmi.set_visible(v)


func _set_world(w: World3D):
for k in _chunks:
var chunk : HT_DirectMultiMeshInstance = _chunks[k]
chunk.set_world(w)
var chunk : Chunk = _chunks[k]
chunk.mmi.set_world(w)


func _on_terrain_transform_changed(gt: Transform3D):
Expand All @@ -424,7 +432,8 @@ func _on_terrain_transform_changed(gt: Transform3D):

# Update AABBs and transforms, because scale might have changed
for k in _chunks:
var mmi : HT_DirectMultiMeshInstance = _chunks[k]
var chunk : Chunk = _chunks[k]
var mmi := chunk.mmi
var aabb := _get_chunk_aabb(terrain, Vector3(k.x * CHUNK_SIZE, 0, k.y * CHUNK_SIZE))
# Nullify XZ translation because that's done by transform already
aabb.position.x = 0
Expand All @@ -440,45 +449,41 @@ func process(delta: float, viewer_pos: Vector3):
return

if _multimesh_need_regen:
print("Regen multimesh")
_regen_multimesh()
_multimesh_need_regen = false
# Crash workaround for Godot 3.1
# See https://github.com/godotengine/godot/issues/32500
for k in _chunks:
var mmi : HT_DirectMultiMeshInstance = _chunks[k]
mmi.set_multimesh(_multimesh)
var chunk : Chunk = _chunks[k]
chunk.mmi.set_multimesh(_multimesh)

# Detail layers are unaffected by ground map_scale
var terrain_transform_without_map_scale : Transform3D = \
terrain.get_internal_transform_unscaled()
var local_viewer_pos := terrain_transform_without_map_scale.affine_inverse() * viewer_pos
var local_viewer_pos : Vector3 = \
terrain_transform_without_map_scale.affine_inverse() * viewer_pos

var viewer_cx := int(local_viewer_pos.x / CHUNK_SIZE)
var viewer_cz := int(local_viewer_pos.z / CHUNK_SIZE)
var viewer_cpos := Vector3i(local_viewer_pos / CHUNK_SIZE)

var cr := int(view_distance) / CHUNK_SIZE + 1

var cmin_x := viewer_cx - cr
var cmin_z := viewer_cz - cr
var cmax_x := viewer_cx + cr
var cmax_z := viewer_cz + cr
var cmin := viewer_cpos - Vector3i(cr, cr, cr)
var cmax := viewer_cpos + Vector3i(cr, cr, cr)

var terrain_data : HTerrainData = terrain.get_data()
var map_res := terrain_data.get_resolution()
var map_scale : Vector3 = terrain.map_scale

var terrain_size_x := map_res * map_scale.x
var terrain_size_z := map_res * map_scale.z

var terrain_chunks_x := terrain_size_x / CHUNK_SIZE
var terrain_chunks_z := terrain_size_z / CHUNK_SIZE

cmin_x = clampi(cmin_x, 0, terrain_chunks_x)
cmin_z = clampi(cmin_z, 0, terrain_chunks_z)

cmax_x = clampi(cmax_x, 0, terrain_chunks_x)
cmax_z = clampi(cmax_z, 0, terrain_chunks_z)

var terrain_size_v2i := Vector2i(map_res * map_scale.x, map_res * map_scale.z)
var terrain_num_chunks_v2i := terrain_size_v2i / CHUNK_SIZE

cmin.x = clampi(cmin.x, 0, terrain_num_chunks_v2i.x)
cmin.z = clampi(cmin.z, 0, terrain_num_chunks_v2i.y)

cmax.x = clampi(cmax.x, 0, terrain_num_chunks_v2i.x)
cmax.z = clampi(cmax.z, 0, terrain_num_chunks_v2i.y)

# This algorithm isn't the most efficient ever.
# Maybe we could switch to a clipbox algorithm eventually, and updating only when the viewer
# changes chunk position?
Expand All @@ -492,41 +497,67 @@ func process(delta: float, viewer_pos: Vector3):
for k in _chunks:
var aabb := _get_chunk_aabb(terrain, Vector3(k.x, 0, k.y) * CHUNK_SIZE)
_add_debug_cube(terrain, aabb, terrain_transform_without_map_scale)

_add_debug_cube(terrain,
AABB(local_viewer_pos - Vector3(1,1,1), Vector3(2,2,2)),
terrain_transform_without_map_scale)

for cz in range(cmin_z, cmax_z):
for cx in range(cmin_x, cmax_x):

var cpos2d := Vector2(cx, cz)
if _chunks.has(cpos2d):
continue

var aabb := _get_chunk_aabb(terrain, Vector3(cx, 0, cz) * CHUNK_SIZE)
var d := _get_approx_distance_to_chunk_aabb(aabb, local_viewer_pos)

if d < view_distance:
_load_chunk(terrain_transform_without_map_scale, cx, cz, aabb)

var to_recycle : Array[Vector2] = []

for k in _chunks:
var chunk = _chunks[k]
var aabb := _get_chunk_aabb(terrain, Vector3(k.x, 0, k.y) * CHUNK_SIZE)
var d := _get_approx_distance_to_chunk_aabb(aabb, local_viewer_pos)
if d > view_distance:
to_recycle.append(k)

for k in to_recycle:
_recycle_chunk(k)

var time_enter := 0
var time_exit := 0

# Only update when the camera moves across chunks
if cmin != _prev_frame_cmin or cmax != _prev_frame_cmax:
for cz in range(cmin.z, cmax.z):
for cx in range(cmin.x, cmax.x):

var cpos2d := Vector2i(cx, cz)
if _chunks.has(cpos2d):
continue

var aabb := _get_chunk_aabb(terrain, Vector3(cx, 0, cz) * CHUNK_SIZE)
var d := _get_approx_distance_to_chunk_aabb(aabb, local_viewer_pos)

if d < view_distance:
_load_chunk(terrain_transform_without_map_scale, cx, cz, aabb)

var to_recycle : Array[Vector2i] = []

for k in _chunks:
var chunk : Chunk = _chunks[k]
if not chunk.pending_update:
chunk.pending_update = true
_pending_exit_updates.append(k)

_process_pending_exit_updates(terrain, local_viewer_pos)

# Update time manually, so we can accelerate the animation when strength is increased,
# without causing phase jumps (which would be the case if we just scaled TIME)
var ambient_wind_frequency = 1.0 + 3.0 * terrain.ambient_wind
_ambient_wind_time += delta * ambient_wind_frequency
var awp = _get_ambient_wind_params()
_material.set_shader_parameter("u_ambient_wind", awp)

_prev_frame_cmin = cmin
_prev_frame_cmax = cmax


func _process_pending_exit_updates(terrain, local_viewer_pos: Vector3):
# Defer this over multiple frames
var budget_us := 1000
var time_before := Time.get_ticks_usec()

while _pending_exit_updates.size() > 0:
var cpos : Vector2i = _pending_exit_updates.pop_back()
var chunk : Chunk = _chunks[cpos]
chunk.pending_update = false

var aabb := _get_chunk_aabb(terrain, Vector3(cpos.x, 0, cpos.y) * CHUNK_SIZE)
var d := _get_approx_distance_to_chunk_aabb(aabb, local_viewer_pos)
if d > view_distance:
_recycle_chunk(cpos)

if Time.get_ticks_usec() - time_before > budget_us:
break


# It would be nice if Godot had "AABB.distance_squared_to(vec3)"...
Expand Down Expand Up @@ -596,15 +627,17 @@ func _load_chunk(terrain_transform_without_map_scale: Transform3D, cx: int, cz:
mmi.set_layer_mask(render_layers)
mmi.set_cast_shadow(cast_shadow)
mmi.set_visible(visible)

_chunks[Vector2(cx, cz)] = mmi

var chunk := Chunk.new()
chunk.mmi = mmi
_chunks[Vector2i(cx, cz)] = chunk


func _recycle_chunk(cpos2d: Vector2):
var mmi : HT_DirectMultiMeshInstance = _chunks[cpos2d]
func _recycle_chunk(cpos2d: Vector2i):
var chunk : Chunk = _chunks[cpos2d]
_chunks.erase(cpos2d)
mmi.set_visible(false)
_multimesh_instance_pool.append(mmi)
chunk.mmi.set_visible(false)
_multimesh_instance_pool.append(chunk.mmi)


func _get_ambient_wind_params() -> Vector2:
Expand Down

0 comments on commit 8c1b660

Please sign in to comment.