diff options
author | Gone2Daly <71726742+Gone2Daly@users.noreply.github.com> | 2023-07-22 21:05:42 +0200 |
---|---|---|
committer | Gone2Daly <71726742+Gone2Daly@users.noreply.github.com> | 2023-07-22 21:05:42 +0200 |
commit | 71b3cd829f80de4c2cd3972d8bfd5ee470a5d180 (patch) | |
tree | b4280fde6eef2ae6987648bc7bf8e00e9011bb7f /game/addons/zylann.hterrain/hterrain.gd | |
parent | ce9022d0df74d6c33db3686622be2050d873ab0b (diff) |
init_testtest3d
Diffstat (limited to 'game/addons/zylann.hterrain/hterrain.gd')
-rw-r--r-- | game/addons/zylann.hterrain/hterrain.gd | 1665 |
1 files changed, 1665 insertions, 0 deletions
diff --git a/game/addons/zylann.hterrain/hterrain.gd b/game/addons/zylann.hterrain/hterrain.gd new file mode 100644 index 0000000..4a186a7 --- /dev/null +++ b/game/addons/zylann.hterrain/hterrain.gd @@ -0,0 +1,1665 @@ +@tool +extends Node3D + +const HT_NativeFactory = preload("./native/factory.gd") +const HT_Mesher = preload("./hterrain_mesher.gd") +const HT_Grid = preload("./util/grid.gd") +const HTerrainData = preload("./hterrain_data.gd") +const HTerrainChunk = preload("./hterrain_chunk.gd") +const HTerrainChunkDebug = preload("./hterrain_chunk_debug.gd") +const HT_Util = preload("./util/util.gd") +const HTerrainCollider = preload("./hterrain_collider.gd") +const HTerrainTextureSet = preload("./hterrain_texture_set.gd") +const HT_Logger = preload("./util/logger.gd") + +const SHADER_CLASSIC4 = "Classic4" +const SHADER_CLASSIC4_LITE = "Classic4Lite" +const SHADER_LOW_POLY = "LowPoly" +const SHADER_ARRAY = "Array" +const SHADER_MULTISPLAT16 = "MultiSplat16" +const SHADER_MULTISPLAT16_LITE = "MultiSplat16Lite" +const SHADER_CUSTOM = "Custom" + +const MIN_MAP_SCALE = 0.01 + +# Note, the `str()` syntax is no longer accepted in constants in Godot 4 +const _SHADER_TYPE_HINT_STRING = \ + "Classic4," + \ + "Classic4Lite," + \ + "LowPoly," + \ + "Array," + \ + "MultiSplat16," + \ + "MultiSplat16Lite," + \ + "Custom" + +# TODO Had to downgrade this to support Godot 3.1. +# Referring to other constants with this syntax isn't working... +#const _SHADER_TYPE_HINT_STRING = str( +# SHADER_CLASSIC4, ",", +# SHADER_CLASSIC4_LITE, ",", +# SHADER_LOW_POLY, ",", +# SHADER_ARRAY, ",", +# SHADER_CUSTOM +#) + +const _builtin_shaders = { + SHADER_CLASSIC4: { + path = "res://addons/zylann.hterrain/shaders/simple4.gdshader", + global_path = "res://addons/zylann.hterrain/shaders/simple4_global.gdshader" + }, + SHADER_CLASSIC4_LITE: { + path = "res://addons/zylann.hterrain/shaders/simple4_lite.gdshader", + global_path = "res://addons/zylann.hterrain/shaders/simple4_global.gdshader" + }, + SHADER_LOW_POLY: { + path = "res://addons/zylann.hterrain/shaders/low_poly.gdshader", + global_path = "" # Not supported + }, + SHADER_ARRAY: { + path = "res://addons/zylann.hterrain/shaders/array.gdshader", + global_path = "res://addons/zylann.hterrain/shaders/array_global.gdshader" + }, + SHADER_MULTISPLAT16: { + path = "res://addons/zylann.hterrain/shaders/multisplat16.gdshader", + global_path = "res://addons/zylann.hterrain/shaders/multisplat16_global.gdshader" + }, + SHADER_MULTISPLAT16_LITE: { + path = "res://addons/zylann.hterrain/shaders/multisplat16_lite.gdshader", + global_path = "res://addons/zylann.hterrain/shaders/multisplat16_global.gdshader" + } +} + +const _NORMAL_BAKER_PATH = "res://addons/zylann.hterrain/tools/normalmap_baker.gd" +const _LOOKDEV_SHADER_PATH = "res://addons/zylann.hterrain/shaders/lookdev.gdshader" + +const SHADER_PARAM_INVERSE_TRANSFORM = "u_terrain_inverse_transform" +const SHADER_PARAM_NORMAL_BASIS = "u_terrain_normal_basis" + +const SHADER_PARAM_GROUND_PREFIX = "u_ground_" # + name + _0, _1, _2, _3... + +# Those parameters are filtered out in the inspector, +# because they are not supposed to be set through it +const _api_shader_params = { + "u_terrain_heightmap": true, + "u_terrain_normalmap": true, + "u_terrain_colormap": true, + "u_terrain_splatmap": true, + "u_terrain_splatmap_1": true, + "u_terrain_splatmap_2": true, + "u_terrain_splatmap_3": true, + "u_terrain_splat_index_map": true, + "u_terrain_splat_weight_map": true, + "u_terrain_globalmap": true, + + "u_terrain_inverse_transform": true, + "u_terrain_normal_basis": true, + + "u_ground_albedo_bump_0": true, + "u_ground_albedo_bump_1": true, + "u_ground_albedo_bump_2": true, + "u_ground_albedo_bump_3": true, + + "u_ground_normal_roughness_0": true, + "u_ground_normal_roughness_1": true, + "u_ground_normal_roughness_2": true, + "u_ground_normal_roughness_3": true, + + "u_ground_albedo_bump_array": true, + "u_ground_normal_roughness_array": true +} + +const _api_shader_ground_albedo_params = { + "u_ground_albedo_bump_0": true, + "u_ground_albedo_bump_1": true, + "u_ground_albedo_bump_2": true, + "u_ground_albedo_bump_3": true +} + +const _ground_texture_array_shader_params = [ + "u_ground_albedo_bump_array", + "u_ground_normal_roughness_array" +] + +const _splatmap_shader_params = [ + "u_terrain_splatmap", + "u_terrain_splatmap_1", + "u_terrain_splatmap_2", + "u_terrain_splatmap_3" +] + +const MIN_CHUNK_SIZE = 16 +const MAX_CHUNK_SIZE = 64 + +# Same as HTerrainTextureSet.get_texture_type_name, used for shader parameter names. +# Indexed by HTerrainTextureSet.TYPE_* +const _ground_enum_to_name = [ + "albedo_bump", + "normal_roughness" +] + +const _DEBUG_AABB = false + +signal transform_changed(global_transform) + +@export_range(0.0, 1.0) var ambient_wind : float: + get: + return ambient_wind + set(amplitude): + if ambient_wind == amplitude: + return + ambient_wind = amplitude + for layer in _detail_layers: + layer.update_material() + + +@export_range(2, 5) var lod_scale := 2.0: + get: + return lod_scale + set(value): + _lodder.set_split_scale(value) + + +# Prefer using this instead of scaling the node's transform. +# Node3D.scale isn't used because it's not suitable for terrains, +# it would scale grass too and other environment objects. +# TODO Replace with `size` in world units? +@export var map_scale := Vector3(1, 1, 1): + get: + return map_scale + set(p_map_scale): + if map_scale == p_map_scale: + return + p_map_scale.x = maxf(p_map_scale.x, MIN_MAP_SCALE) + p_map_scale.y = maxf(p_map_scale.y, MIN_MAP_SCALE) + p_map_scale.z = maxf(p_map_scale.z, MIN_MAP_SCALE) + map_scale = p_map_scale + _on_transform_changed() + + +@export var centered := false: + get: + return centered + set(p_centered): + if p_centered == centered: + return + centered = p_centered + _on_transform_changed() + + +var _custom_shader : Shader = null +var _custom_globalmap_shader : Shader = null +var _shader_type := SHADER_CLASSIC4_LITE +var _shader_uses_texture_array := false +var _material := ShaderMaterial.new() +var _material_params_need_update := false +# Possible values are the same as the enum `GeometryInstance.SHADOW_CASTING_SETTING_*`. +var _cast_shadow_setting := GeometryInstance3D.SHADOW_CASTING_SETTING_ON + +var _render_layer_mask := 1 + +# Actual number of textures supported by the shader currently selected +var _ground_texture_count_cache := 0 + +var _used_splatmaps_count_cache := 0 +var _is_using_indexed_splatmap := false + +var _texture_set := HTerrainTextureSet.new() +var _texture_set_migration_textures = null + +var _data: HTerrainData = null + +var _mesher := HT_Mesher.new() +var _lodder = HT_NativeFactory.get_quad_tree_lod() +var _viewer_pos_world := Vector3() + +# [lod][z][x] -> chunk +# This container owns chunks +var _chunks := [] +var _chunk_size: int = 32 +var _pending_chunk_updates := [] + +var _detail_layers := [] + +var _collision_enabled := true +var _collider: HTerrainCollider = null +var _collision_layer := 1 +var _collision_mask := 1 + +# Stats & debug +var _updated_chunks := 0 +var _logger = HT_Logger.get_for(self) + +# Editor-only +var _normals_baker = null + +var _lookdev_enabled := false +var _lookdev_material : ShaderMaterial + + +func _init(): + _logger.debug("Create HeightMap") + # This sets up the defaults. They may be overridden shortly after by the scene loader. + + _lodder.set_callbacks(_cb_make_chunk, _cb_recycle_chunk, _cb_get_vertical_bounds) + + set_notify_transform(true) + + # TODO Temporary! + # This is a workaround for https://github.com/godotengine/godot/issues/24488 + _material.set_shader_parameter("u_ground_uv_scale", 20) + _material.set_shader_parameter("u_ground_uv_scale_vec4", Color(20, 20, 20, 20)) + _material.set_shader_parameter("u_depth_blending", true) + + _material.shader = load(_builtin_shaders[_shader_type].path) + + _texture_set.changed.connect(_on_texture_set_changed) + + if _collision_enabled: + if _check_heightmap_collider_support(): + _collider = HTerrainCollider.new(self, _collision_layer, _collision_mask) + + +func _get_property_list(): + # A lot of properties had to be exported like this instead of using `export`, + # because Godot 3 does not support easy categorization and lacks some hints + var props = [ + { + # Terrain data is exposed only as a path in the editor, + # because it can only be saved if it has a directory selected. + # That property is not used in scene saving (data is instead). + "name": "data_directory", + "type": TYPE_STRING, + "usage": PROPERTY_USAGE_EDITOR, + "hint": PROPERTY_HINT_DIR + }, + { + # The actual data resource is only exposed for storage. + # I had to name it so that Godot won't try to assign _data directly + # instead of using the setter I made... + "name": "_terrain_data", + "type": TYPE_OBJECT, + "usage": PROPERTY_USAGE_STORAGE, + "hint": PROPERTY_HINT_RESOURCE_TYPE, + # This actually triggers `ERROR: Cannot get class`, + # if it were to be shown in the inspector. + # See https://github.com/godotengine/godot/pull/41264 + "hint_string": "HTerrainData" + }, + { + "name": "chunk_size", + "type": TYPE_INT, + "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE, + #"hint": PROPERTY_HINT_ENUM, + "hint_string": "16, 32" + }, + { + "name": "Collision", + "type": TYPE_NIL, + "usage": PROPERTY_USAGE_GROUP + }, + { + "name": "collision_enabled", + "type": TYPE_BOOL, + "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE + }, + { + "name": "collision_layer", + "type": TYPE_INT, + "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE, + "hint": PROPERTY_HINT_LAYERS_3D_PHYSICS + }, + { + "name": "collision_mask", + "type": TYPE_INT, + "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE, + "hint": PROPERTY_HINT_LAYERS_3D_PHYSICS + }, + { + "name": "Rendering", + "type": TYPE_NIL, + "usage": PROPERTY_USAGE_GROUP + }, + { + "name": "shader_type", + "type": TYPE_STRING, + "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE, + "hint": PROPERTY_HINT_ENUM, + "hint_string": _SHADER_TYPE_HINT_STRING + }, + { + "name": "custom_shader", + "type": TYPE_OBJECT, + "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE, + "hint": PROPERTY_HINT_RESOURCE_TYPE, + "hint_string": "Shader" + }, + { + "name": "custom_globalmap_shader", + "type": TYPE_OBJECT, + "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE, + "hint": PROPERTY_HINT_RESOURCE_TYPE, + "hint_string": "Shader" + }, + { + "name": "texture_set", + "type": TYPE_OBJECT, + "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE, + "hint": PROPERTY_HINT_RESOURCE_TYPE, + "hint_string": "Resource" + # TODO Cannot properly hint the type of the resource in the inspector. + # This triggers `ERROR: Cannot get class 'HTerrainTextureSet'` + # See https://github.com/godotengine/godot/pull/41264 + #"hint_string": "HTerrainTextureSet" + }, + { + "name": "render_layers", + "type": TYPE_INT, + "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE, + "hint": PROPERTY_HINT_LAYERS_3D_RENDER + }, + { + "name": "cast_shadow", + "type": TYPE_INT, + "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE, + "hint": PROPERTY_HINT_ENUM, + "hint_string": "Off,On,DoubleSided,ShadowsOnly" + } + ] + + if _material.shader != null: + var shader_params := RenderingServer.get_shader_parameter_list(_material.shader.get_rid()) + for p in shader_params: + if _api_shader_params.has(p.name): + continue + var cp := {} + for k in p: + cp[k] = p[k] + cp.name = str("shader_params/", p.name) + props.append(cp) + + return props + + +func _get(key: StringName): + if key == &"data_directory": + return _get_data_directory() + + if key == &"_terrain_data": + if _data == null or _data.resource_path == "": + # Consider null if the data is not set or has no path, + # because in those cases we can't save the terrain properly + return null + else: + return _data + + if key == &"texture_set": + return get_texture_set() + + elif key == &"shader_type": + return get_shader_type() + + elif key == &"custom_shader": + return get_custom_shader() + + elif key == &"custom_globalmap_shader": + return _custom_globalmap_shader + + elif key.begins_with("shader_params/"): + var param_name := key.substr(len("shader_params/")) + return get_shader_param(param_name) + + elif key == &"chunk_size": + return _chunk_size + + elif key == &"collision_enabled": + return _collision_enabled + + elif key == &"collision_layer": + return _collision_layer + + elif key == &"collision_mask": + return _collision_mask + + elif key == &"render_layers": + return get_render_layer_mask() + + elif key == &"cast_shadow": + return _cast_shadow_setting + + +func _set(key: StringName, value): + if key == &"data_directory": + _set_data_directory(value) + + # Can't use setget when the exported type is custom, + # because we were also are forced to use _get_property_list... + elif key == &"_terrain_data": + set_data(value) + + elif key == &"texture_set": + set_texture_set(value) + + # Legacy, left for migration from 1.4 + var key_str := String(key) + if key_str.begins_with("ground/"): + for ground_texture_type in HTerrainTextureSet.TYPE_COUNT: + var type_name = _ground_enum_to_name[ground_texture_type] + if key_str.begins_with(str("ground/", type_name, "_")): + var i = key_str.substr(len(key_str) - 1).to_int() + if _texture_set_migration_textures == null: + _texture_set_migration_textures = [] + while i >= len(_texture_set_migration_textures): + _texture_set_migration_textures.append([null, null]) + var texs = _texture_set_migration_textures[i] + texs[ground_texture_type] = value + + elif key == &"shader_type": + set_shader_type(value) + + elif key == &"custom_shader": + set_custom_shader(value) + + elif key == &"custom_globalmap_shader": + _custom_globalmap_shader = value + + elif key.begins_with("shader_params/"): + var param_name := String(key).substr(len("shader_params/")) + set_shader_param(param_name, value) + + elif key == &"chunk_size": + set_chunk_size(value) + + elif key == &"collision_enabled": + set_collision_enabled(value) + + elif key == &"collision_layer": + _collision_layer = value + if _collider != null: + _collider.set_collision_layer(value) + + elif key == &"collision_mask": + _collision_mask = value + if _collider != null: + _collider.set_collision_mask(value) + + elif key == &"render_layers": + return set_render_layer_mask(value) + + elif key == &"cast_shadow": + set_cast_shadow(value) + + +func get_texture_set() -> HTerrainTextureSet: + return _texture_set + + +func set_texture_set(new_set: HTerrainTextureSet): + if _texture_set == new_set: + return + + if _texture_set != null: + # TODO This causes `ERROR: Nonexistent signal 'changed' in [Resource:36653]` for some reason + _texture_set.changed.disconnect(_on_texture_set_changed) + + _texture_set = new_set + + if _texture_set != null: + _texture_set.changed.connect(_on_texture_set_changed) + + _material_params_need_update = true + + +func _on_texture_set_changed(): + _material_params_need_update = true + HT_Util.update_configuration_warning(self, false) + + +func get_shader_param(param_name: String): + return _material.get_shader_parameter(param_name) + + +func set_shader_param(param_name: String, v): + _material.set_shader_parameter(param_name, v) + + +func set_render_layer_mask(mask: int): + _render_layer_mask = mask + _for_all_chunks(HT_SetRenderLayerMaskAction.new(mask)) + + +func get_render_layer_mask() -> int: + return _render_layer_mask + + +func set_cast_shadow(setting: int): + if setting == _cast_shadow_setting: + return + _cast_shadow_setting = setting + _for_all_chunks(HT_SetCastShadowSettingAction.new(setting)) + + +func get_cast_shadow() -> int: + return _cast_shadow_setting + + +func _set_data_directory(dirpath: String): + if dirpath != _get_data_directory(): + if dirpath == "": + set_data(null) + else: + var fpath := dirpath.path_join(HTerrainData.META_FILENAME) + if FileAccess.file_exists(fpath): + # Load existing + var d = load(fpath) + set_data(d) + else: + # Create new + var d := HTerrainData.new() + d.resource_path = fpath + set_data(d) + else: + _logger.warn("Setting twice the same terrain directory??") + + +func _get_data_directory() -> String: + if _data != null: + return _data.resource_path.get_base_dir() + return "" + + +func _check_heightmap_collider_support() -> bool: + return true + # var v = Engine.get_version_info() + # if v.major == 3 and v.minor == 0 and v.patch < 4: + # _logger.error("Heightmap collision shape not supported in this version of Godot," + # + " please upgrade to 3.0.4 or later") + # return false + # return true + + +func set_collision_enabled(enabled: bool): + if _collision_enabled != enabled: + _collision_enabled = enabled + if _collision_enabled: + if _check_heightmap_collider_support(): + _collider = HTerrainCollider.new(self, _collision_layer, _collision_mask) + # Collision is not updated with data here, + # because loading is quite a mess at the moment... + # 1) This function can be called while no data has been set yet + # 2) I don't want to update the collider more times than necessary + # because it's expensive + # 3) I would prefer not defer that to the moment the terrain is + # added to the tree, because it would screw up threaded loading + else: + # Despite this object being a Reference, + # this should free it, as it should be the only reference + _collider = null + + +func _for_all_chunks(action): + for lod in len(_chunks): + var grid = _chunks[lod] + for y in len(grid): + var row = grid[y] + for x in len(row): + var chunk = row[x] + if chunk != null: + action.exec(chunk) + + +func get_chunk_size() -> int: + return _chunk_size + + +func set_chunk_size(p_cs: int): + assert(typeof(p_cs) == TYPE_INT) + _logger.debug(str("Setting chunk size to ", p_cs)) + var cs := HT_Util.next_power_of_two(p_cs) + if cs < MIN_CHUNK_SIZE: + cs = MIN_CHUNK_SIZE + if cs > MAX_CHUNK_SIZE: + cs = MAX_CHUNK_SIZE + if p_cs != cs: + _logger.debug(str("Chunk size snapped to ", cs)) + if cs == _chunk_size: + return + _chunk_size = cs + _reset_ground_chunks() + + +# Compat +func set_map_scale(p_map_scale: Vector3): + map_scale = p_map_scale + + +# Compat +func set_centered(p_centered: bool): + centered = p_centered + + +# Gets the global transform to apply to terrain geometry, +# which is different from Node3D.global_transform gives. +# global_transform must only have translation and rotation. Scale support is undefined. +func get_internal_transform() -> Transform3D: + var gt := global_transform + var it := Transform3D(gt.basis * Basis().scaled(map_scale), gt.origin) + if centered and _data != null: + var half_size := 0.5 * (_data.get_resolution() - 1.0) + it.origin += it.basis * (-Vector3(half_size, 0, half_size)) + return it + + +func get_internal_transform_unscaled(): + var gt := global_transform + if centered and _data != null: + var half_size := 0.5 * (_data.get_resolution() - 1.0) + gt.origin += gt.basis * (-Vector3(half_size, 0, half_size)) + return gt + + +# Converts a world-space position into a map-space position. +# Map space X and Z coordinates correspond to pixel coordinates of the heightmap. +func world_to_map(world_pos: Vector3) -> Vector3: + return get_internal_transform().affine_inverse() * world_pos + + +func _notification(what: int): + match what: + NOTIFICATION_PREDELETE: + _logger.debug("Destroy HTerrain") + # Note: might get rid of a circular ref in GDScript port + _clear_all_chunks() + + NOTIFICATION_ENTER_WORLD: + _logger.debug("Enter world") + + if _texture_set_migration_textures != null and _texture_set.get_slots_count() == 0: + # Convert from 1.4 textures properties to HTerrainTextureSet + # TODO Unfortunately this might not always work, + # once again because Godot wants the editor's UndoRedo to have modified the + # resource for it to be saved... which sucks, sucks, and sucks. + # I'll never say it enough. + _texture_set.set_mode(HTerrainTextureSet.MODE_TEXTURES) + while _texture_set.get_slots_count() < len(_texture_set_migration_textures): + _texture_set.insert_slot(-1) + for slot_index in len(_texture_set_migration_textures): + var texs = _texture_set_migration_textures[slot_index] + for type in len(texs): + _texture_set.set_texture(slot_index, type, texs[type]) + _texture_set_migration_textures = null + + _for_all_chunks(HT_EnterWorldAction.new(get_world_3d())) + if _collider != null: + _collider.set_world(get_world_3d()) + _collider.set_transform(get_internal_transform()) + + NOTIFICATION_EXIT_WORLD: + _logger.debug("Exit world") + _for_all_chunks(HT_ExitWorldAction.new()) + if _collider != null: + _collider.set_world(null) + + NOTIFICATION_TRANSFORM_CHANGED: + _on_transform_changed() + + NOTIFICATION_VISIBILITY_CHANGED: + _logger.debug("Visibility changed") + _for_all_chunks(HT_VisibilityChangedAction.new(is_visible_in_tree())) + + +func _on_transform_changed(): + _logger.debug("Transform changed") + + if not is_inside_tree(): + # The transform and other properties can be set by the scene loader, + # before we enter the tree + return + + var gt = get_internal_transform() + + _for_all_chunks(HT_TransformChangedAction.new(gt)) + + _material_params_need_update = true + + if _collider != null: + _collider.set_transform(gt) + + transform_changed.emit(gt) + + +func _enter_tree(): + _logger.debug("Enter tree") + + if Engine.is_editor_hint() and _normals_baker == null: + _normals_baker = load(_NORMAL_BAKER_PATH).new() + add_child(_normals_baker) + _normals_baker.set_terrain_data(_data) + + set_process(true) + + +func _clear_all_chunks(): + # The lodder has to be cleared because otherwise it will reference dangling pointers + _lodder.clear() + + #_for_all_chunks(DeleteChunkAction.new()) + + for i in len(_chunks): + _chunks[i].clear() + + +func _get_chunk_at(pos_x: int, pos_y: int, lod: int) -> HTerrainChunk: + if lod < len(_chunks): + return HT_Grid.grid_get_or_default(_chunks[lod], pos_x, pos_y, null) + return null + + +func get_data() -> HTerrainData: + return _data + + +func has_data() -> bool: + return _data != null + + +func set_data(new_data: HTerrainData): + assert(new_data == null or new_data is HTerrainData) + + _logger.debug(str("Set new data ", new_data)) + + if _data == new_data: + return + + if has_data(): + _logger.debug("Disconnecting old HeightMapData") + _data.resolution_changed.disconnect(_on_data_resolution_changed) + _data.region_changed.disconnect(_on_data_region_changed) + _data.map_changed.disconnect(_on_data_map_changed) + _data.map_added.disconnect(_on_data_map_added) + _data.map_removed.disconnect(_on_data_map_removed) + + if _normals_baker != null: + _normals_baker.set_terrain_data(null) + _normals_baker.queue_free() + _normals_baker = null + + _data = new_data + + # Note: the order of these two is important + _clear_all_chunks() + + if has_data(): + _logger.debug("Connecting new HeightMapData") + + # This is a small UX improvement so that the user sees a default terrain + if is_inside_tree() and Engine.is_editor_hint(): + if _data.get_resolution() == 0: + _data._edit_load_default() + + if _collider != null: + _collider.create_from_terrain_data(_data) + + _data.resolution_changed.connect(_on_data_resolution_changed) + _data.region_changed.connect(_on_data_region_changed) + _data.map_changed.connect(_on_data_map_changed) + _data.map_added.connect(_on_data_map_added) + _data.map_removed.connect(_on_data_map_removed) + + if _normals_baker != null: + _normals_baker.set_terrain_data(_data) + + _on_data_resolution_changed() + + _material_params_need_update = true + + HT_Util.update_configuration_warning(self, true) + + _logger.debug("Set data done") + + +# The collider might be used in editor for other tools (like snapping to floor), +# so the whole collider can be updated in one go. +# It may be slow for ingame use, so prefer calling it when appropriate. +func update_collider(): + assert(_collision_enabled) + assert(_collider != null) + _collider.create_from_terrain_data(_data) + + +func _on_data_resolution_changed(): + _reset_ground_chunks() + + +func _reset_ground_chunks(): + if _data == null: + return + + _clear_all_chunks() + + _pending_chunk_updates.clear() + + _lodder.create_from_sizes(_chunk_size, _data.get_resolution()) + + _chunks.resize(_lodder.get_lod_count()) + + var cres := _data.get_resolution() / _chunk_size + var csize_x := cres + var csize_y := cres + + for lod in _lodder.get_lod_count(): + _logger.debug(str("Create grid for lod ", lod, ", ", csize_x, "x", csize_y)) + var grid = HT_Grid.create_grid(csize_x, csize_y) + _chunks[lod] = grid + csize_x /= 2 + csize_y /= 2 + + _mesher.configure(_chunk_size, _chunk_size, _lodder.get_lod_count()) + + +func _on_data_region_changed(min_x, min_y, size_x, size_y, channel): + # Testing only heights because it's the only channel that can impact geometry and LOD + if channel == HTerrainData.CHANNEL_HEIGHT: + set_area_dirty(min_x, min_y, size_x, size_y) + + if _normals_baker != null: + _normals_baker.request_tiles_in_region(Vector2(min_x, min_y), Vector2(size_x, size_y)) + + +func _on_data_map_changed(type: int, index: int): + if type == HTerrainData.CHANNEL_DETAIL \ + or type == HTerrainData.CHANNEL_HEIGHT \ + or type == HTerrainData.CHANNEL_NORMAL \ + or type == HTerrainData.CHANNEL_GLOBAL_ALBEDO: + + for layer in _detail_layers: + layer.update_material() + + if type != HTerrainData.CHANNEL_DETAIL: + _material_params_need_update = true + + +func _on_data_map_added(type: int, index: int): + if type == HTerrainData.CHANNEL_DETAIL: + for layer in _detail_layers: + # Shift indexes up since one was inserted + if layer.layer_index >= index: + layer.layer_index += 1 + layer.update_material() + else: + _material_params_need_update = true + HT_Util.update_configuration_warning(self, true) + + +func _on_data_map_removed(type: int, index: int): + if type == HTerrainData.CHANNEL_DETAIL: + for layer in _detail_layers: + # Shift indexes down since one was removed + if layer.layer_index > index: + layer.layer_index -= 1 + layer.update_material() + else: + _material_params_need_update = true + HT_Util.update_configuration_warning(self, true) + + +func get_shader_type() -> String: + return _shader_type + + +func set_shader_type(type: String): + if type == _shader_type: + return + _shader_type = type + + if _shader_type == SHADER_CUSTOM: + _material.shader = _custom_shader + else: + _material.shader = load(_builtin_shaders[_shader_type].path) + + _material_params_need_update = true + + if Engine.is_editor_hint(): + notify_property_list_changed() + + +func get_custom_shader() -> Shader: + return _custom_shader + + +func set_custom_shader(shader: Shader): + if _custom_shader == shader: + return + + if _custom_shader != null: + _custom_shader.changed.disconnect(_on_custom_shader_changed) + + if Engine.is_editor_hint() and shader != null and is_inside_tree(): + # When the new shader is empty, allow to fork from the previous shader + if shader.code.is_empty(): + _logger.debug("Populating custom shader with default code") + var src := _material.shader + if src == null: + src = load(_builtin_shaders[SHADER_CLASSIC4].path) + shader.code = src.code + # TODO If code isn't empty, + # verify existing parameters and issue a warning if important ones are missing + + _custom_shader = shader + + if _shader_type == SHADER_CUSTOM: + _material.shader = _custom_shader + + if _custom_shader != null: + _custom_shader.changed.connect(_on_custom_shader_changed) + if _shader_type == SHADER_CUSTOM: + _material_params_need_update = true + + if Engine.is_editor_hint(): + notify_property_list_changed() + + +func _on_custom_shader_changed(): + _material_params_need_update = true + + +func _update_material_params(): + assert(_material != null) + _logger.debug("Updating terrain material params") + + var terrain_textures := {} + var res := Vector2(-1, -1) + + var lookdev_material : ShaderMaterial + if _lookdev_enabled: + lookdev_material = _get_lookdev_material() + + # TODO Only get textures the shader supports + + if has_data(): + for map_type in HTerrainData.CHANNEL_COUNT: + var count := _data.get_map_count(map_type) + for i in count: + var param_name: String = HTerrainData.get_map_shader_param_name(map_type, i) + terrain_textures[param_name] = _data.get_texture(map_type, i) + res.x = _data.get_resolution() + res.y = res.x + + # Set all parameters from the terrain system. + + if is_inside_tree(): + var gt := get_internal_transform() + var t := gt.affine_inverse() + _material.set_shader_parameter(SHADER_PARAM_INVERSE_TRANSFORM, t) + + # This is needed to properly transform normals if the terrain is scaled + var normal_basis = gt.basis.inverse().transposed() + _material.set_shader_parameter(SHADER_PARAM_NORMAL_BASIS, normal_basis) + + if lookdev_material != null: + lookdev_material.set_shader_parameter(SHADER_PARAM_INVERSE_TRANSFORM, t) + lookdev_material.set_shader_parameter(SHADER_PARAM_NORMAL_BASIS, normal_basis) + + for param_name in terrain_textures: + var tex = terrain_textures[param_name] + _material.set_shader_parameter(param_name, tex) + if lookdev_material != null: + lookdev_material.set_shader_parameter(param_name, tex) + + if _texture_set != null: + match _texture_set.get_mode(): + HTerrainTextureSet.MODE_TEXTURES: + var slots_count := _texture_set.get_slots_count() + for type in HTerrainTextureSet.TYPE_COUNT: + for slot_index in slots_count: + var texture := _texture_set.get_texture(slot_index, type) + var shader_param := _get_ground_texture_shader_param_name(type, slot_index) + _material.set_shader_parameter(shader_param, texture) + + HTerrainTextureSet.MODE_TEXTURE_ARRAYS: + for type in HTerrainTextureSet.TYPE_COUNT: + var texture_array := _texture_set.get_texture_array(type) + var shader_params := _get_ground_texture_array_shader_param_name(type) + _material.set_shader_parameter(shader_params, texture_array) + + _shader_uses_texture_array = false + _is_using_indexed_splatmap = false + _used_splatmaps_count_cache = 0 + + var shader := _material.shader + if shader != null: + var param_list := RenderingServer.get_shader_parameter_list(shader.get_rid()) + _ground_texture_count_cache = 0 + for p in param_list: + if _api_shader_ground_albedo_params.has(p.name): + _ground_texture_count_cache += 1 + elif p.name == "u_ground_albedo_bump_array": + _shader_uses_texture_array = true + elif p.name == "u_terrain_splat_index_map": + _is_using_indexed_splatmap = true + elif p.name in _splatmap_shader_params: + _used_splatmaps_count_cache += 1 + + +# TODO Rename is_shader_using_texture_array() +# Tells if the current shader is using a texture array. +# This will only be valid once the material has been updated internally. +# (for example it won't be valid before the terrain is added to the SceneTree) +func is_using_texture_array() -> bool: + return _shader_uses_texture_array + + +# Gets how many splatmaps the current shader is using. +# This will only be valid once the material has been updated internally. +# (for example it won't be valid before the terrain is added to the SceneTree) +func get_used_splatmaps_count() -> int: + return _used_splatmaps_count_cache + + +# Tells if the current shader is using a splatmap type based on indexes and weights. +# This will only be valid once the material has been updated internally. +# (for example it won't be valid before the terrain is added to the SceneTree) +func is_using_indexed_splatmap() -> bool: + return _is_using_indexed_splatmap + + +static func _get_common_shader_params(shader1: Shader, shader2: Shader) -> Array: + var shader1_param_names := {} + var common_params := [] + + var shader1_params := RenderingServer.get_shader_parameter_list(shader1.get_rid()) + var shader2_params := RenderingServer.get_shader_parameter_list(shader2.get_rid()) + + for p in shader1_params: + shader1_param_names[p.name] = true + + for p in shader2_params: + if shader1_param_names.has(p.name): + common_params.append(p.name) + + return common_params + + +# Helper used for globalmap baking +func setup_globalmap_material(mat: ShaderMaterial): + mat.shader = get_globalmap_shader() + if mat.shader == null: + _logger.error("Could not find a shader to use for baking the global map.") + return + # Copy all parameters shaders have in common + var common_params = _get_common_shader_params(mat.shader, _material.shader) + for param_name in common_params: + var v = _material.get_shader_parameter(param_name) + mat.set_shader_parameter(param_name, v) + + +# Gets which shader will be used to bake the globalmap +func get_globalmap_shader() -> Shader: + if _shader_type == SHADER_CUSTOM: + if _custom_globalmap_shader != null: + return _custom_globalmap_shader + _logger.warn("The terrain uses a custom shader but doesn't have one for baking the " + + "global map. Will attempt to use a built-in shader.") + if is_using_texture_array(): + return load(_builtin_shaders[SHADER_ARRAY].global_path) as Shader + return load(_builtin_shaders[SHADER_CLASSIC4].global_path) as Shader + return load(_builtin_shaders[_shader_type].global_path) as Shader + + +# Compat +func set_lod_scale(p_lod_scale: float): + lod_scale = p_lod_scale + + +# Compat +func get_lod_scale() -> float: + return lod_scale + + +func get_lod_count() -> int: + return _lodder.get_lod_count() + + +# 3 +# o---o +# 0 | | 1 +# o---o +# 2 +# Directions to go to neighbor chunks +const s_dirs = [ + [-1, 0], # SEAM_LEFT + [1, 0], # SEAM_RIGHT + [0, -1], # SEAM_BOTTOM + [0, 1] # SEAM_TOP +] + +# 7 6 +# o---o---o +# 0 | | 5 +# o o +# 1 | | 4 +# o---o---o +# 2 3 +# +# Directions to go to neighbor chunks of higher LOD +const s_rdirs = [ + [-1, 0], + [-1, 1], + [0, 2], + [1, 2], + [2, 1], + [2, 0], + [1, -1], + [0, -1] +] + + +func _edit_update_viewer_position(camera: Camera3D): + _update_viewer_position(camera) + + +func _update_viewer_position(camera: Camera3D): + if camera == null: + var viewport := get_viewport() + if viewport != null: + camera = viewport.get_camera_3d() + + if camera == null: + return + + if camera.projection == Camera3D.PROJECTION_ORTHOGONAL: + # In this mode, due to the fact Godot does not allow negative near plane, + # users have to pull the camera node very far away, but it confuses LOD + # into very low detail, while the seen area remains the same. + # So we need to base LOD on a different metric. + var cam_pos := camera.global_transform.origin + var cam_dir := -camera.global_transform.basis.z + var max_distance := camera.far * 1.2 + var hit_cell_pos = cell_raycast(cam_pos, cam_dir, max_distance) + + if hit_cell_pos != null: + var cell_to_world := get_internal_transform() + var h := _data.get_height_at(hit_cell_pos.x, hit_cell_pos.y) + _viewer_pos_world = cell_to_world * Vector3(hit_cell_pos.x, h, hit_cell_pos.y) + + else: + _viewer_pos_world = camera.global_transform.origin + + +func _process(delta: float): + if not Engine.is_editor_hint(): + # In editor, the camera is only accessible from an editor plugin + _update_viewer_position(null) + + if has_data(): + if _data.is_locked(): + # Can't use the data for now + return + + if _data.get_resolution() != 0: + var gt := get_internal_transform() + # Viewer position such that 1 unit == 1 pixel in the heightmap + var viewer_pos_heightmap_local := gt.affine_inverse() * _viewer_pos_world + #var time_before = OS.get_ticks_msec() + _lodder.update(viewer_pos_heightmap_local) + #var time_elapsed = OS.get_ticks_msec() - time_before + #if Engine.get_frames_drawn() % 60 == 0: + # _logger.debug(str("Lodder time: ", time_elapsed)) + + if _data.get_map_count(HTerrainData.CHANNEL_DETAIL) > 0: + # Note: the detail system is not affected by map scale, + # so we have to send viewer position in world space + for layer in _detail_layers: + layer.process(delta, _viewer_pos_world) + + _updated_chunks = 0 + + # Add more chunk updates for neighboring (seams): + # This adds updates to higher-LOD chunks around lower-LOD ones, + # because they might not needed to update by themselves, but the fact a neighbor + # chunk got joined or split requires them to create or revert seams + var precount = _pending_chunk_updates.size() + for i in precount: + var u: HT_PendingChunkUpdate = _pending_chunk_updates[i] + + # In case the chunk got split + for d in 4: + var ncpos_x = u.pos_x + s_dirs[d][0] + var ncpos_y = u.pos_y + s_dirs[d][1] + + var nchunk := _get_chunk_at(ncpos_x, ncpos_y, u.lod) + if nchunk != null and nchunk.is_active(): + # Note: this will append elements to the array we are iterating on, + # but we iterate only on the previous count so it should be fine + _add_chunk_update(nchunk, ncpos_x, ncpos_y, u.lod) + + # In case the chunk got joined + if u.lod > 0: + var cpos_upper_x := u.pos_x * 2 + var cpos_upper_y := u.pos_y * 2 + var nlod := u.lod - 1 + + for rd in 8: + var ncpos_upper_x = cpos_upper_x + s_rdirs[rd][0] + var ncpos_upper_y = cpos_upper_y + s_rdirs[rd][1] + + var nchunk := _get_chunk_at(ncpos_upper_x, ncpos_upper_y, nlod) + if nchunk != null and nchunk.is_active(): + _add_chunk_update(nchunk, ncpos_upper_x, ncpos_upper_y, nlod) + + # Update chunks + var lvisible := is_visible_in_tree() + for i in len(_pending_chunk_updates): + var u: HT_PendingChunkUpdate = _pending_chunk_updates[i] + var chunk := _get_chunk_at(u.pos_x, u.pos_y, u.lod) + assert(chunk != null) + _update_chunk(chunk, u.lod, lvisible) + _updated_chunks += 1 + + _pending_chunk_updates.clear() + + if _material_params_need_update: + _update_material_params() + HT_Util.update_configuration_warning(self, false) + _material_params_need_update = false + + # DEBUG +# if(_updated_chunks > 0): +# _logger.debug(str("Updated {0} chunks".format(_updated_chunks))) + + +func _update_chunk(chunk: HTerrainChunk, lod: int, p_visible: bool): + assert(has_data()) + + # Check for my own seams + var seams := 0 + var cpos_x := chunk.cell_origin_x / (_chunk_size << lod) + var cpos_y := chunk.cell_origin_y / (_chunk_size << lod) + var cpos_lower_x := cpos_x / 2 + var cpos_lower_y := cpos_y / 2 + + # Check for lower-LOD chunks around me + for d in 4: + var ncpos_lower_x = (cpos_x + s_dirs[d][0]) / 2 + var ncpos_lower_y = (cpos_y + s_dirs[d][1]) / 2 + if ncpos_lower_x != cpos_lower_x or ncpos_lower_y != cpos_lower_y: + var nchunk := _get_chunk_at(ncpos_lower_x, ncpos_lower_y, lod + 1) + if nchunk != null and nchunk.is_active(): + seams |= (1 << d) + + var mesh := _mesher.get_chunk(lod, seams) + chunk.set_mesh(mesh) + + # Because chunks are rendered using vertex shader displacement, + # the renderer cannot rely on the mesh's AABB. + var s := _chunk_size << lod + var aabb := _data.get_region_aabb(chunk.cell_origin_x, chunk.cell_origin_y, s, s) + aabb.position.x = 0 + aabb.position.z = 0 + chunk.set_aabb(aabb) + + chunk.set_visible(p_visible) + chunk.set_pending_update(false) + + +func _add_chunk_update(chunk: HTerrainChunk, pos_x: int, pos_y: int, lod: int): + if chunk.is_pending_update(): + #_logger.debug("Chunk update is already pending!") + return + + assert(lod < len(_chunks)) + assert(pos_x >= 0) + assert(pos_y >= 0) + assert(pos_y < len(_chunks[lod])) + assert(pos_x < len(_chunks[lod][pos_y])) + + # No update pending for this chunk, create one + var u := HT_PendingChunkUpdate.new() + u.pos_x = pos_x + u.pos_y = pos_y + u.lod = lod + _pending_chunk_updates.push_back(u) + + chunk.set_pending_update(true) + + # TODO Neighboring chunks might need an update too + # because of normals and seams being updated + + +# Used when editing an existing terrain +func set_area_dirty(origin_in_cells_x: int, origin_in_cells_y: int, \ + size_in_cells_x: int, size_in_cells_y: int): + + var cpos0_x := origin_in_cells_x / _chunk_size + var cpos0_y := origin_in_cells_y / _chunk_size + var csize_x := (size_in_cells_x - 1) / _chunk_size + 1 + var csize_y := (size_in_cells_y - 1) / _chunk_size + 1 + + # For each lod + for lod in _lodder.get_lod_count(): + # Get grid and chunk size + var grid = _chunks[lod] + var s : int = _lodder.get_lod_factor(lod) + + # Convert rect into this lod's coordinates: + # Pick min and max (included), divide them, then add 1 to max so it's excluded again + var min_x := cpos0_x / s + var min_y := cpos0_y / s + var max_x := (cpos0_x + csize_x - 1) / s + 1 + var max_y := (cpos0_y + csize_y - 1) / s + 1 + + # Find which chunks are within + for cy in range(min_y, max_y): + for cx in range(min_x, max_x): + var chunk = HT_Grid.grid_get_or_default(grid, cx, cy, null) + if chunk != null and chunk.is_active(): + _add_chunk_update(chunk, cx, cy, lod) + + +# Called when a chunk is needed to be seen +func _cb_make_chunk(cpos_x: int, cpos_y: int, lod: int): + # TODO What if cpos is invalid? _get_chunk_at will return NULL but that's still invalid + var chunk := _get_chunk_at(cpos_x, cpos_y, lod) + + if chunk == null: + # This is the first time this chunk is required at this lod, generate it + + var lod_factor : int = _lodder.get_lod_factor(lod) + var origin_in_cells_x := cpos_x * _chunk_size * lod_factor + var origin_in_cells_y := cpos_y * _chunk_size * lod_factor + + var material = _material + if _lookdev_enabled: + material = _get_lookdev_material() + + if _DEBUG_AABB: + chunk = HTerrainChunkDebug.new( + self, origin_in_cells_x, origin_in_cells_y, material) + else: + chunk = HTerrainChunk.new(self, origin_in_cells_x, origin_in_cells_y, material) + chunk.parent_transform_changed(get_internal_transform()) + + chunk.set_render_layer_mask(_render_layer_mask) + chunk.set_cast_shadow_setting(_cast_shadow_setting) + + var grid = _chunks[lod] + var row = grid[cpos_y] + row[cpos_x] = chunk + + # Make sure it gets updated + _add_chunk_update(chunk, cpos_x, cpos_y, lod) + + chunk.set_active(true) + return chunk + + +# Called when a chunk is no longer seen +func _cb_recycle_chunk(chunk: HTerrainChunk, cx: int, cy: int, lod: int): + chunk.set_visible(false) + chunk.set_active(false) + + +func _cb_get_vertical_bounds(cpos_x: int, cpos_y: int, lod: int): + var chunk_size : int = _chunk_size * _lodder.get_lod_factor(lod) + var origin_in_cells_x := cpos_x * chunk_size + var origin_in_cells_y := cpos_y * chunk_size + # This is a hack for speed, + # because the proper algorithm appears to be too slow for GDScript. + # It should be good enough for most common cases, unless you have super-sharp cliffs. + return _data.get_point_aabb( + origin_in_cells_x + chunk_size / 2, + origin_in_cells_y + chunk_size / 2) +# var aabb = _data.get_region_aabb( +# origin_in_cells_x, origin_in_cells_y, chunk_size, chunk_size) +# return Vector2(aabb.position.y, aabb.end.y) + + +# static func _get_height_or_default(im: Image, pos_x: int, pos_y: int): +# if pos_x < 0 or pos_y < 0 or pos_x >= im.get_width() or pos_y >= im.get_height(): +# return 0.0 +# return im.get_pixel(pos_x, pos_y).r + + +# Performs a raycast to the terrain without using the collision engine. +# This is mostly useful in the editor, where the collider can't be updated in realtime. +# Returns cell hit position as Vector2, or null if there was no hit. +# TODO Cannot type hint nullable return value +func cell_raycast(origin_world: Vector3, dir_world: Vector3, max_distance: float): + assert(typeof(origin_world) == TYPE_VECTOR3) + assert(typeof(dir_world) == TYPE_VECTOR3) + if not has_data(): + return null + # Transform to local (takes map scale into account) + var to_local := get_internal_transform().affine_inverse() + var origin = to_local * origin_world + var dir = to_local.basis * dir_world + return _data.cell_raycast(origin, dir, max_distance) + + +static func _get_ground_texture_shader_param_name(ground_texture_type: int, slot: int) -> String: + assert(typeof(slot) == TYPE_INT and slot >= 0) + _check_ground_texture_type(ground_texture_type) + return str(SHADER_PARAM_GROUND_PREFIX, _ground_enum_to_name[ground_texture_type], "_", slot) + + +# @obsolete +func get_ground_texture(slot: int, type: int) -> Texture: + _logger.error( + "HTerrain.get_ground_texture is obsolete, " + + "use HTerrain.get_texture_set().get_texture(slot, type) instead") + var shader_param = _get_ground_texture_shader_param_name(type, slot) + return _material.get_shader_parameter(shader_param) + + +# @obsolete +func set_ground_texture(slot: int, type: int, tex: Texture): + _logger.error( + "HTerrain.set_ground_texture is obsolete, " + + "use HTerrain.get_texture_set().set_texture(slot, type, texture) instead") + assert(tex == null or tex is Texture) + var shader_param := _get_ground_texture_shader_param_name(type, slot) + _material.set_shader_parameter(shader_param, tex) + + +func _get_ground_texture_array_shader_param_name(type: int) -> String: + return _ground_texture_array_shader_params[type] as String + + +# @obsolete +func get_ground_texture_array(type: int) -> TextureLayered: + _logger.error( + "HTerrain.get_ground_texture_array is obsolete, " + + "use HTerrain.get_texture_set().get_texture_array(type) instead") + var param_name := _get_ground_texture_array_shader_param_name(type) + return _material.get_shader_parameter(param_name) + + +# @obsolete +func set_ground_texture_array(type: int, texture_array: TextureLayered): + _logger.error( + "HTerrain.set_ground_texture_array is obsolete, " + + "use HTerrain.get_texture_set().set_texture_array(type, texarray) instead") + var param_name := _get_ground_texture_array_shader_param_name(type) + _material.set_shader_parameter(param_name, texture_array) + + +func _internal_add_detail_layer(layer): + assert(_detail_layers.find(layer) == -1) + _detail_layers.append(layer) + + +func _internal_remove_detail_layer(layer): + assert(_detail_layers.find(layer) != -1) + _detail_layers.erase(layer) + + +# Returns a list copy of all child HTerrainDetailLayer nodes. +# The order in that list has no relevance. +func get_detail_layers() -> Array: + return _detail_layers.duplicate() + + +# @obsolete +func set_detail_texture(slot, tex): + _logger.error( + "HTerrain.set_detail_texture is obsolete, use HTerrainDetailLayer.texture instead") + + +# @obsolete +func get_detail_texture(slot): + _logger.error( + "HTerrain.get_detail_texture is obsolete, use HTerrainDetailLayer.texture instead") + + +# Compat +func set_ambient_wind(amplitude: float): + ambient_wind = amplitude + + +static func _check_ground_texture_type(ground_texture_type: int): + assert(typeof(ground_texture_type) == TYPE_INT) + assert(ground_texture_type >= 0 and ground_texture_type < HTerrainTextureSet.TYPE_COUNT) + + +# @obsolete +func get_ground_texture_slot_count() -> int: + _logger.error("get_ground_texture_slot_count is obsolete, " \ + + "use get_cached_ground_texture_slot_count instead") + return get_max_ground_texture_slot_count() + +# @obsolete +func get_max_ground_texture_slot_count() -> int: + _logger.error("get_ground_texture_slot_count is obsolete, " \ + + "use get_cached_ground_texture_slot_count instead") + return get_cached_ground_texture_slot_count() + + +# This is a cached value based on the actual number of texture parameters +# in the current shader. It won't update immediately when the shader changes, +# only after a frame. This is mostly used in the editor. +func get_cached_ground_texture_slot_count() -> int: + return _ground_texture_count_cache + + +func _edit_debug_draw(ci: CanvasItem): + _lodder.debug_draw_tree(ci) + + +func _get_configuration_warnings() -> PackedStringArray: + var warnings := PackedStringArray() + + if _data == null: + warnings.append("The terrain is missing data.\n" \ + + "Select the `Data Directory` property in the inspector to assign it.") + + if _texture_set == null: + warnings.append("The terrain does not have a HTerrainTextureSet assigned\n" \ + + "This is required if you want to paint textures on it.") + + else: + var mode := _texture_set.get_mode() + + if mode == HTerrainTextureSet.MODE_TEXTURES and is_using_texture_array(): + warnings.append("The current shader needs texture arrays,\n" \ + + "but the current HTerrainTextureSet is setup with individual textures.\n" \ + + "You may need to switch it to TEXTURE_ARRAYS mode,\n" \ + + "or re-import images in this mode with the import tool.") + + elif mode == HTerrainTextureSet.MODE_TEXTURE_ARRAYS and not is_using_texture_array(): + warnings.append("The current shader needs individual textures,\n" \ + + "but the current HTerrainTextureSet is setup with texture arrays.\n" \ + + "You may need to switch it to TEXTURES mode,\n" \ + + "or re-import images in this mode with the import tool.") + + # TODO Warn about unused data maps, have a tool to clean them up + return warnings + + +func set_lookdev_enabled(enable: bool): + if _lookdev_enabled == enable: + return + _lookdev_enabled = enable + _material_params_need_update = true + if _lookdev_enabled: + _for_all_chunks(HT_SetMaterialAction.new(_get_lookdev_material())) + else: + _for_all_chunks(HT_SetMaterialAction.new(_material)) + + +func set_lookdev_shader_param(param_name: String, value): + var mat = _get_lookdev_material() + mat.set_shader_parameter(param_name, value) + + +func is_lookdev_enabled() -> bool: + return _lookdev_enabled + + +func _get_lookdev_material() -> ShaderMaterial: + if _lookdev_material == null: + _lookdev_material = ShaderMaterial.new() + _lookdev_material.shader = load(_LOOKDEV_SHADER_PATH) + return _lookdev_material + + +class HT_PendingChunkUpdate: + var pos_x := 0 + var pos_y := 0 + var lod := 0 + + +class HT_EnterWorldAction: + var world : World3D = null + func _init(w): + world = w + func exec(chunk): + chunk.enter_world(world) + + +class HT_ExitWorldAction: + func exec(chunk): + chunk.exit_world() + + +class HT_TransformChangedAction: + var transform : Transform3D + func _init(t): + transform = t + func exec(chunk): + chunk.parent_transform_changed(transform) + + +class HT_VisibilityChangedAction: + var visible := false + func _init(v): + visible = v + func exec(chunk): + chunk.set_visible(visible and chunk.is_active()) + + +#class HT_DeleteChunkAction: +# func exec(chunk): +# pass + + +class HT_SetMaterialAction: + var material : Material = null + func _init(m): + material = m + func exec(chunk): + chunk.set_material(material) + + +class HT_SetRenderLayerMaskAction: + var mask: int = 0 + func _init(m: int): + mask = m + func exec(chunk): + chunk.set_render_layer_mask(mask) + + +class HT_SetCastShadowSettingAction: + var setting := 0 + func _init(s: int): + setting = s + func exec(chunk): + chunk.set_cast_shadow_setting(setting) |