diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e0cc08..9d3cb8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/addons/zylann.hterrain/hterrain_detail_layer.gd b/addons/zylann.hterrain/hterrain_detail_layer.gd index 05847e3..2ea80ab 100644 --- a/addons/zylann.hterrain/hterrain_detail_layer.gd +++ b/addons/zylann.hterrain/hterrain_detail_layer.gd @@ -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. @@ -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 @@ -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(): @@ -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): @@ -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 @@ -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? @@ -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)"... @@ -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: