## Converts raw ADTLoader data into a Godot Node3D terrain scene. extends RefCounted class_name ADTBuilder const WOW_LIQUID_MATERIAL := preload("res://addons/mpq_extractor/loaders/wow_liquid_material.gd") const TILE_SIZE := 533.33333 const CHUNK_SIZE := 33.33333 const UNIT_SIZE := CHUNK_SIZE / 8.0 const IGNORE_TERRAIN_HOLES := false const USE_GEOMETRY_NORMALS := true static var _terrain_shader: Shader static var _control_splat_shader: Shader static var _single_texture_shader: Shader static var _empty_alpha_texture: Texture2D static var _native_adt_baker: Object static var _fallback_material: StandardMaterial3D static var _single_texture_material_cache: Dictionary = {} static var _baked_texture_material_cache: Dictionary = {} static var _alpha_texture_cache: Dictionary = {} static var _layered_material_cache: Dictionary = {} ## Build a Node3D with 256 terrain chunk meshes. ## `extracted_dir` must be an absolute path to the extracted/ folder so BLPs can be loaded. static func build(data: Dictionary, extracted_dir: String = "") -> Node3D: var root := Node3D.new() root.name = "ADT" var tile_origin := get_tile_origin(data) root.position = tile_origin var tex_names: PackedStringArray = data.get("textures", PackedStringArray()) var tex_cache: Dictionary = {} for chunk in data.get("chunks", []) as Array: if chunk.is_empty(): continue var mi := _build_chunk_mesh(chunk, tex_names, tex_cache, extracted_dir, 0, tile_origin) if mi: root.add_child(mi) var water_root := build_tile_water_scene(data, tile_origin) if water_root: root.add_child(water_root) return root func build_scene(data: Dictionary, extracted_dir: String = "") -> Node3D: return build(data, extracted_dir) func get_tile_origin_for_data(data: Dictionary) -> Vector3: return get_tile_origin(data) func build_chunk_scene( chunk: Dictionary, tex_names: PackedStringArray, tex_cache: Dictionary, extracted_dir: String = "", lod: int = 0, origin_offset: Vector3 = Vector3.ZERO) -> MeshInstance3D: return _build_chunk_mesh(chunk, tex_names, tex_cache, extracted_dir, lod, origin_offset) func build_chunk_render_payload( chunk: Dictionary, tex_names: PackedStringArray, tex_cache: Dictionary, extracted_dir: String = "", lod: int = 0, origin_offset: Vector3 = Vector3.ZERO) -> Dictionary: return build_chunk_payload(chunk, tex_names, tex_cache, extracted_dir, lod, origin_offset) static func build_tile_water_scene(data: Dictionary, origin_offset: Vector3 = Vector3.ZERO) -> Node3D: return _build_tile_water_root(data, origin_offset) static func build_tile_coarse_scene( data: Dictionary, tex_cache: Dictionary, extracted_dir: String = "", lod: int = 3) -> MeshInstance3D: return _build_tile_coarse_mesh(data, tex_cache, extracted_dir, lod) static func build_chunk_payload( chunk: Dictionary, tex_names: PackedStringArray, tex_cache: Dictionary, extracted_dir: String = "", lod: int = 0, origin_offset: Vector3 = Vector3.ZERO) -> Dictionary: return _build_chunk_payload(chunk, tex_names, tex_cache, extracted_dir, lod, origin_offset) static func build_tile_coarse_render_payload( data: Dictionary, tex_cache: Dictionary, extracted_dir: String = "", lod: int = 3) -> Dictionary: return _build_tile_coarse_payload(data, tex_cache, extracted_dir, lod) static func build_baked_tile_render_payload( data: Dictionary, image_cache: Dictionary, extracted_dir: String = "", lod: int = 0, texture_size: int = 512) -> Dictionary: return _build_baked_tile_payload(data, image_cache, extracted_dir, lod, texture_size) static func build_control_splat_tile_render_payload( data: Dictionary, image_cache: Dictionary, extracted_dir: String = "", lod: int = 0, texture_size: int = 256) -> Dictionary: return _build_control_splat_tile_payload(data, image_cache, extracted_dir, lod, texture_size) static func apply_control_splat_material( mesh: Mesh, texture_images: Array, alpha_atlas: Texture2D, layer_index_map: Texture2D) -> Mesh: if mesh == null or texture_images.is_empty() or alpha_atlas == null or layer_index_map == null: return mesh var texture_array := _build_terrain_texture_array_from_images(texture_images) if texture_array == null: return mesh var array_mesh := mesh as ArrayMesh if array_mesh == null: return mesh array_mesh.surface_set_material( 0, _build_control_splat_material(texture_array, alpha_atlas, layer_index_map)) return array_mesh static func build_baked_tile_render_payload_pair( data: Dictionary, image_cache: Dictionary, extracted_dir: String = "", full_lod: int = 0, full_texture_size: int = 2048, coarse_lod: int = 3, coarse_texture_size: int = 512) -> Dictionary: return _build_baked_tile_payload_pair( data, image_cache, extracted_dir, full_lod, full_texture_size, coarse_lod, coarse_texture_size) static func upgrade_cached_baked_mesh_materials( mesh: Mesh, uv_scale: float = 1.0, mip_bias: float = -1.25, sharpen_strength: float = 0.35) -> Mesh: var array_mesh := mesh as ArrayMesh if array_mesh == null: return mesh for surface_idx in array_mesh.get_surface_count(): var mat := array_mesh.surface_get_material(surface_idx) if not (mat is ShaderMaterial): continue var shader_mat := mat as ShaderMaterial var tex: Texture2D = shader_mat.get_shader_parameter("tex0") if tex == null: continue var upgraded := ShaderMaterial.new() upgraded.shader = _get_single_texture_shader() upgraded.set_shader_parameter("uv_scale", uv_scale) upgraded.set_shader_parameter("mip_bias", mip_bias) upgraded.set_shader_parameter("sharpen_strength", sharpen_strength) upgraded.set_shader_parameter("tex0", tex) array_mesh.surface_set_material(surface_idx, upgraded) return array_mesh static func get_tile_origin(data: Dictionary) -> Vector3: var found := false var min_x := 0.0 var min_z := 0.0 for chunk in data.get("chunks", []) as Array: if chunk.is_empty(): continue var origin: Vector3 = chunk.get("origin", Vector3.ZERO) if not found: min_x = origin.x min_z = origin.z found = true else: min_x = min(min_x, origin.x) min_z = min(min_z, origin.z) return Vector3(min_x, 0.0, min_z) if found else Vector3.ZERO static func _outer(row: int, col: int) -> int: return row * 17 + col static func _inner(row: int, col: int) -> int: return row * 17 + 9 + col static func _build_chunk_mesh( chunk: Dictionary, tex_names: PackedStringArray, tex_cache: Dictionary, extracted_dir: String, lod: int = 0, origin_offset: Vector3 = Vector3.ZERO) -> MeshInstance3D: var payload := _build_chunk_payload(chunk, tex_names, tex_cache, extracted_dir, lod, origin_offset) return _build_mesh_instance_from_payload(payload) static func _build_chunk_payload( chunk: Dictionary, tex_names: PackedStringArray, tex_cache: Dictionary, extracted_dir: String, lod: int = 0, origin_offset: Vector3 = Vector3.ZERO) -> Dictionary: var heights: PackedFloat32Array = chunk.get("heights", PackedFloat32Array()) var origin: Vector3 = chunk.get("origin", Vector3.ZERO) var layers: Array = chunk.get("layers", []) var alpha_maps: Array = chunk.get("alpha_maps", []) var positions := _build_chunk_positions(heights) if positions.is_empty(): return {} var geometry := _build_chunk_geometry( positions, chunk.get("normals", PackedVector3Array()), chunk.get("holes", 0), lod) var verts: PackedVector3Array = geometry["verts"] if verts.is_empty(): return {} var nrms: PackedVector3Array = geometry["nrms"] var uvs_arr: PackedVector2Array = geometry["uvs"] var indices: PackedInt32Array = geometry["indices"] var arrays := [] arrays.resize(Mesh.ARRAY_MAX) arrays[Mesh.ARRAY_VERTEX] = verts arrays[Mesh.ARRAY_NORMAL] = nrms arrays[Mesh.ARRAY_TEX_UV] = uvs_arr arrays[Mesh.ARRAY_INDEX] = indices var mesh := ArrayMesh.new() mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) mesh.surface_set_material( 0, _build_chunk_material(layers, alpha_maps, tex_names, tex_cache, extracted_dir, lod)) return { "mesh": mesh, "position": origin - origin_offset, "name": "Chunk_%d_%d" % [chunk.get("index_x", 0), chunk.get("index_y", 0)], } static func _build_chunk_positions(heights: PackedFloat32Array) -> PackedVector3Array: if heights.size() < 145: return PackedVector3Array() var positions := PackedVector3Array() positions.resize(145) for r in range(9): for c in range(9): var idx := _outer(r, c) positions[idx] = Vector3(c * UNIT_SIZE, heights[idx], r * UNIT_SIZE) for r in range(8): for c in range(8): var idx := _inner(r, c) positions[idx] = Vector3( c * UNIT_SIZE + UNIT_SIZE * 0.5, heights[idx], r * UNIT_SIZE + UNIT_SIZE * 0.5) return positions static func _build_chunk_geometry( positions: PackedVector3Array, normals: PackedVector3Array, holes: int, lod: int) -> Dictionary: if IGNORE_TERRAIN_HOLES: holes = 0 var verts := PackedVector3Array() var nrms := PackedVector3Array() var uvs_arr := PackedVector2Array() var indices := PackedInt32Array() if lod < 3: _append_full_chunk_geometry(positions, normals, holes, verts, nrms, uvs_arr, indices) else: var step: int = 1 << (lod - 1) # LOD1→step=1, LOD2→step=2 _append_outer_grid_geometry(positions, normals, holes, 8, verts, nrms, uvs_arr, indices) return { "verts": verts, "nrms": nrms, "uvs": uvs_arr, "indices": indices, } static func _append_full_chunk_geometry( positions: PackedVector3Array, normals: PackedVector3Array, holes: int, verts: PackedVector3Array, nrms: PackedVector3Array, uvs_arr: PackedVector2Array, indices: PackedInt32Array) -> void: for iy in range(8): for ix in range(8): var hx := ix / 2 var hy := iy / 2 if holes & (1 << (hy * 4 + hx)): continue var tl := _outer(iy, ix) var tr := _outer(iy, ix + 1) var bl := _outer(iy + 1, ix) var br := _outer(iy + 1, ix + 1) var mid := _inner(iy, ix) for tri: Array in [[mid, tl, bl], [mid, bl, br], [mid, br, tr], [mid, tr, tl]]: _append_triangle(tri, positions, normals, verts, nrms, uvs_arr, indices) static func _append_outer_grid_geometry( positions: PackedVector3Array, normals: PackedVector3Array, holes: int, step: int, verts: PackedVector3Array, nrms: PackedVector3Array, uvs_arr: PackedVector2Array, indices: PackedInt32Array) -> void: for iy in range(0, 8, step): for ix in range(0, 8, step): if _coarse_cell_has_hole(holes, ix, iy, step): continue var tl := _outer(iy, ix) var tr := _outer(iy, ix + step) var bl := _outer(iy + step, ix) var br := _outer(iy + step, ix + step) _append_triangle([tl, bl, br], positions, normals, verts, nrms, uvs_arr, indices) _append_triangle([tl, br, tr], positions, normals, verts, nrms, uvs_arr, indices) static func _coarse_cell_has_hole(holes: int, start_x: int, start_y: int, step: int) -> bool: if step >= 8: return false for cell_y in range(start_y, start_y + step): for cell_x in range(start_x, start_x + step): var hx := cell_x / 2 var hy := cell_y / 2 if holes & (1 << (hy * 4 + hx)): return true return false static func _append_triangle( tri: Array, positions: PackedVector3Array, normals: PackedVector3Array, verts: PackedVector3Array, nrms: PackedVector3Array, uvs_arr: PackedVector2Array, indices: PackedInt32Array) -> void: var base := verts.size() var p0: Vector3 = positions[int(tri[0])] var p1: Vector3 = positions[int(tri[1])] var p2: Vector3 = positions[int(tri[2])] var face_normal := (p1 - p0).cross(p2 - p0).normalized() if USE_GEOMETRY_NORMALS and face_normal.y < 0.0: face_normal = -face_normal for vi: int in tri: var p: Vector3 = positions[vi] verts.append(p) uvs_arr.append(Vector2(p.x / CHUNK_SIZE, p.z / CHUNK_SIZE)) if USE_GEOMETRY_NORMALS: nrms.append(face_normal if face_normal.length_squared() > 0.0 else Vector3.UP) else: nrms.append(normals[vi] if vi < normals.size() else Vector3.UP) indices.append(base) indices.append(base + 1) indices.append(base + 2) static func _build_tile_coarse_mesh( data: Dictionary, tex_cache: Dictionary, extracted_dir: String, lod: int) -> MeshInstance3D: var payload := _build_tile_coarse_payload(data, tex_cache, extracted_dir, lod) return _build_mesh_instance_from_payload(payload) static func _build_tile_coarse_payload( data: Dictionary, tex_cache: Dictionary, extracted_dir: String, lod: int) -> Dictionary: var tile_origin := get_tile_origin(data) var tex_names: PackedStringArray = data.get("textures", PackedStringArray()) var surfaces: Dictionary = {} for chunk in data.get("chunks", []) as Array: if chunk.is_empty(): continue var heights: PackedFloat32Array = chunk.get("heights", PackedFloat32Array()) var positions := _build_chunk_positions(heights) if positions.is_empty(): continue var geometry := _build_chunk_geometry( positions, chunk.get("normals", PackedVector3Array()), chunk.get("holes", 0), lod) var verts: PackedVector3Array = geometry["verts"] if verts.is_empty(): continue var material: Material = _build_chunk_material( chunk.get("layers", []), chunk.get("alpha_maps", []), tex_names, tex_cache, extracted_dir, lod) var mat_key := material.get_instance_id() if not surfaces.has(mat_key): surfaces[mat_key] = { "material": material, "verts": PackedVector3Array(), "nrms": PackedVector3Array(), "uvs": PackedVector2Array(), "indices": PackedInt32Array(), } var surface: Dictionary = surfaces[mat_key] var offset: Vector3 = chunk.get("origin", Vector3.ZERO) - tile_origin var dst_verts: PackedVector3Array = surface["verts"] var dst_nrms: PackedVector3Array = surface["nrms"] var dst_uvs: PackedVector2Array = surface["uvs"] var dst_indices: PackedInt32Array = surface["indices"] var base := dst_verts.size() for v: Vector3 in verts: dst_verts.append(v + offset) for n: Vector3 in geometry["nrms"]: dst_nrms.append(n) for uv: Vector2 in geometry["uvs"]: dst_uvs.append(uv) for idx: int in geometry["indices"]: dst_indices.append(base + idx) surface["verts"] = dst_verts surface["nrms"] = dst_nrms surface["uvs"] = dst_uvs surface["indices"] = dst_indices surfaces[mat_key] = surface if surfaces.is_empty(): return {} var mesh := ArrayMesh.new() for surface in surfaces.values(): var arrays := [] arrays.resize(Mesh.ARRAY_MAX) arrays[Mesh.ARRAY_VERTEX] = surface["verts"] arrays[Mesh.ARRAY_NORMAL] = surface["nrms"] arrays[Mesh.ARRAY_TEX_UV] = surface["uvs"] arrays[Mesh.ARRAY_INDEX] = surface["indices"] mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) mesh.surface_set_material(mesh.get_surface_count() - 1, surface["material"]) return { "mesh": mesh, "position": Vector3.ZERO, "name": "TileLOD%d" % lod, } static func _build_baked_tile_payload( data: Dictionary, image_cache: Dictionary, extracted_dir: String, lod: int, texture_size: int) -> Dictionary: var payload := _build_baked_tile_geometry_payload(data, lod) if payload.is_empty(): return {} var baked_image := _build_tile_baked_albedo(data, image_cache, extracted_dir, texture_size) _apply_baked_albedo_material(payload.get("mesh", null), baked_image) return payload static func _build_control_splat_tile_payload( data: Dictionary, image_cache: Dictionary, extracted_dir: String, lod: int, texture_size: int) -> Dictionary: var payload := _build_baked_tile_geometry_payload(data, lod) if payload.is_empty(): return {} var tex_names: PackedStringArray = data.get("textures", PackedStringArray()) var texture_images := _build_terrain_texture_images(tex_names, image_cache, extracted_dir, texture_size) var texture_array := _build_terrain_texture_array_from_images(texture_images) if texture_array == null: return payload payload = _build_control_splat_tile_geometry_payload(data, lod, tex_names.size()) if payload.is_empty(): return {} var alpha_atlas := _build_tile_alpha_atlas(data) var layer_index_map := _build_tile_layer_index_map(data, tex_names.size()) var material := _build_control_splat_material(texture_array, alpha_atlas, layer_index_map) var mesh: Mesh = payload.get("mesh", null) if mesh != null: mesh.surface_set_material(0, material) payload["texture_array"] = texture_array payload["texture_images"] = texture_images payload["alpha_atlas"] = alpha_atlas payload["layer_index_map"] = layer_index_map payload["texture_names"] = tex_names return payload static func _build_control_splat_tile_geometry_payload(data: Dictionary, lod: int, texture_count: int) -> Dictionary: var tile_origin := get_tile_origin(data) var verts := PackedVector3Array() var nrms := PackedVector3Array() var uvs_arr := PackedVector2Array() var colors := PackedColorArray() var indices := PackedInt32Array() var max_id := mini(maxi(texture_count - 1, 0), 255) for chunk in data.get("chunks", []) as Array: if chunk.is_empty(): continue var heights: PackedFloat32Array = chunk.get("heights", PackedFloat32Array()) var positions := _build_chunk_positions(heights) if positions.is_empty(): continue var geometry := _build_chunk_geometry( positions, chunk.get("normals", PackedVector3Array()), chunk.get("holes", 0), lod) var local_verts: PackedVector3Array = geometry["verts"] if local_verts.is_empty(): continue var layer_color := _get_chunk_layer_color(chunk, max_id) var offset: Vector3 = chunk.get("origin", Vector3.ZERO) - tile_origin var base := verts.size() for v: Vector3 in local_verts: var tv := v + offset verts.append(tv) uvs_arr.append(Vector2(tv.x / TILE_SIZE, tv.z / TILE_SIZE)) colors.append(layer_color) for n: Vector3 in geometry["nrms"]: nrms.append(n) for idx: int in geometry["indices"]: indices.append(base + idx) if verts.is_empty(): return {} var arrays := [] arrays.resize(Mesh.ARRAY_MAX) arrays[Mesh.ARRAY_VERTEX] = verts arrays[Mesh.ARRAY_NORMAL] = nrms arrays[Mesh.ARRAY_TEX_UV] = uvs_arr arrays[Mesh.ARRAY_COLOR] = colors arrays[Mesh.ARRAY_INDEX] = indices var mesh := ArrayMesh.new() mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) return { "mesh": mesh, "position": Vector3.ZERO, "name": "ControlSplatTileLOD%d" % lod, } static func _build_baked_tile_payload_pair( data: Dictionary, image_cache: Dictionary, extracted_dir: String, full_lod: int, full_texture_size: int, coarse_lod: int, coarse_texture_size: int) -> Dictionary: var full_payload := _build_baked_tile_geometry_payload(data, full_lod) if full_payload.is_empty(): return {} var coarse_payload := _build_baked_tile_geometry_payload(data, coarse_lod) var full_image := _build_tile_baked_albedo(data, image_cache, extracted_dir, full_texture_size) var coarse_image: Image = null if full_image != null: coarse_image = full_image.duplicate() var coarse_size := _get_baked_albedo_size(coarse_texture_size) if coarse_image.get_width() != coarse_size or coarse_image.get_height() != coarse_size: coarse_image.resize(coarse_size, coarse_size, Image.INTERPOLATE_BILINEAR) _apply_baked_albedo_material(full_payload.get("mesh", null), full_image) if not coarse_payload.is_empty(): _apply_baked_albedo_material(coarse_payload.get("mesh", null), coarse_image) return { "full": full_payload, "coarse": coarse_payload, } static func _build_baked_tile_geometry_payload(data: Dictionary, lod: int) -> Dictionary: var tile_origin := get_tile_origin(data) var verts := PackedVector3Array() var nrms := PackedVector3Array() var uvs_arr := PackedVector2Array() var indices := PackedInt32Array() for chunk in data.get("chunks", []) as Array: if chunk.is_empty(): continue var heights: PackedFloat32Array = chunk.get("heights", PackedFloat32Array()) var positions := _build_chunk_positions(heights) if positions.is_empty(): continue var geometry := _build_chunk_geometry( positions, chunk.get("normals", PackedVector3Array()), chunk.get("holes", 0), lod) var local_verts: PackedVector3Array = geometry["verts"] if local_verts.is_empty(): continue var offset: Vector3 = chunk.get("origin", Vector3.ZERO) - tile_origin var base := verts.size() for v: Vector3 in local_verts: var tv := v + offset verts.append(tv) uvs_arr.append(Vector2(tv.x / TILE_SIZE, tv.z / TILE_SIZE)) for n: Vector3 in geometry["nrms"]: nrms.append(n) for idx: int in geometry["indices"]: indices.append(base + idx) if verts.is_empty(): return {} var arrays := [] arrays.resize(Mesh.ARRAY_MAX) arrays[Mesh.ARRAY_VERTEX] = verts arrays[Mesh.ARRAY_NORMAL] = nrms arrays[Mesh.ARRAY_TEX_UV] = uvs_arr arrays[Mesh.ARRAY_INDEX] = indices var mesh := ArrayMesh.new() mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) return { "mesh": mesh, "position": Vector3.ZERO, "name": "TileLOD%d" % lod, } static func _apply_baked_albedo_material(mesh: Mesh, baked_image: Image) -> void: if mesh == null: return if baked_image: baked_image.generate_mipmaps() var baked_texture := ImageTexture.create_from_image(baked_image) mesh.surface_set_material(0, _build_baked_texture_material(baked_texture)) else: mesh.surface_set_material(0, _build_fallback_material()) static func _build_mesh_instance_from_payload(payload: Dictionary) -> MeshInstance3D: if payload.is_empty(): return null var mi := MeshInstance3D.new() mi.mesh = payload.get("mesh", null) mi.position = payload.get("position", Vector3.ZERO) mi.name = payload.get("name", "TerrainMesh") return mi static func _build_tile_water_root(data: Dictionary, origin_offset: Vector3) -> Node3D: var root := Node3D.new() root.name = "Water" var surfaces: Dictionary = {} for chunk in data.get("chunks", []) as Array: if chunk.is_empty(): continue for liquid in chunk.get("liquids", []) as Array: if liquid.is_empty(): continue var liquid_id := int(liquid.get("liquid_id", 0)) if not surfaces.has(liquid_id): surfaces[liquid_id] = { "verts": PackedVector3Array(), "nrms": PackedVector3Array(), "uvs": PackedVector2Array(), "indices": PackedInt32Array(), } _append_liquid_geometry(chunk, liquid, origin_offset, surfaces[liquid_id]) for liquid_id in surfaces.keys(): var surface: Dictionary = surfaces[liquid_id] var verts: PackedVector3Array = surface["verts"] if verts.is_empty(): continue var arrays := [] arrays.resize(Mesh.ARRAY_MAX) arrays[Mesh.ARRAY_VERTEX] = verts arrays[Mesh.ARRAY_NORMAL] = surface["nrms"] arrays[Mesh.ARRAY_TEX_UV] = surface["uvs"] arrays[Mesh.ARRAY_INDEX] = surface["indices"] var mesh := ArrayMesh.new() mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) mesh.surface_set_material(0, _build_liquid_material(int(liquid_id))) var mi := MeshInstance3D.new() mi.mesh = mesh mi.name = "Liquid_%d" % int(liquid_id) mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF root.add_child(mi) return root if root.get_child_count() > 0 else null static func _append_liquid_geometry( chunk: Dictionary, liquid: Dictionary, origin_offset: Vector3, surface: Dictionary) -> void: var liquid_mask: PackedByteArray = liquid.get("mask", PackedByteArray()) var liquid_heights: PackedFloat32Array = liquid.get("heights", PackedFloat32Array()) if liquid_mask.size() < 8 * 8 or liquid_heights.size() < 9 * 9: return var chunk_origin: Vector3 = chunk.get("origin", Vector3.ZERO) var tile_local_origin := chunk_origin - origin_offset var verts: PackedVector3Array = surface["verts"] var nrms: PackedVector3Array = surface["nrms"] var uvs_arr: PackedVector2Array = surface["uvs"] var indices: PackedInt32Array = surface["indices"] for z in range(8): for x in range(8): if liquid_mask[z * 8 + x] == 0: continue var base := verts.size() var x0 := tile_local_origin.x + x * UNIT_SIZE var x1 := tile_local_origin.x + (x + 1) * UNIT_SIZE var z0 := tile_local_origin.z + z * UNIT_SIZE var z1 := tile_local_origin.z + (z + 1) * UNIT_SIZE var y00 := liquid_heights[z * 9 + x] - origin_offset.y var y10 := liquid_heights[z * 9 + x + 1] - origin_offset.y var y01 := liquid_heights[(z + 1) * 9 + x] - origin_offset.y var y11 := liquid_heights[(z + 1) * 9 + x + 1] - origin_offset.y var u0 := x0 / TILE_SIZE var u1 := x1 / TILE_SIZE var v0 := z0 / TILE_SIZE var v1 := z1 / TILE_SIZE verts.append(Vector3(x0, y00, z0)) verts.append(Vector3(x0, y01, z1)) verts.append(Vector3(x1, y11, z1)) verts.append(Vector3(x1, y10, z0)) nrms.append(Vector3.UP) nrms.append(Vector3.UP) nrms.append(Vector3.UP) nrms.append(Vector3.UP) uvs_arr.append(Vector2(u0, v0)) uvs_arr.append(Vector2(u0, v1)) uvs_arr.append(Vector2(u1, v1)) uvs_arr.append(Vector2(u1, v0)) indices.append(base) indices.append(base + 1) indices.append(base + 2) indices.append(base) indices.append(base + 2) indices.append(base + 3) surface["verts"] = verts surface["nrms"] = nrms surface["uvs"] = uvs_arr surface["indices"] = indices static func _build_liquid_mesh( chunk: Dictionary, liquid: Dictionary, origin_offset: Vector3) -> MeshInstance3D: var liquid_mask: PackedByteArray = liquid.get("mask", PackedByteArray()) var liquid_heights: PackedFloat32Array = liquid.get("heights", PackedFloat32Array()) if liquid_mask.size() < 8 * 8 or liquid_heights.size() < 9 * 9: return null var chunk_origin: Vector3 = chunk.get("origin", Vector3.ZERO) var verts := PackedVector3Array() var nrms := PackedVector3Array() var uvs_arr := PackedVector2Array() var indices := PackedInt32Array() for z in range(8): for x in range(8): if liquid_mask[z * 8 + x] == 0: continue var base := verts.size() var y00 := liquid_heights[z * 9 + x] - chunk_origin.y var y10 := liquid_heights[z * 9 + x + 1] - chunk_origin.y var y01 := liquid_heights[(z + 1) * 9 + x] - chunk_origin.y var y11 := liquid_heights[(z + 1) * 9 + x + 1] - chunk_origin.y verts.append(Vector3(x * UNIT_SIZE, y00, z * UNIT_SIZE)) verts.append(Vector3(x * UNIT_SIZE, y01, (z + 1) * UNIT_SIZE)) verts.append(Vector3((x + 1) * UNIT_SIZE, y11, (z + 1) * UNIT_SIZE)) verts.append(Vector3((x + 1) * UNIT_SIZE, y10, z * UNIT_SIZE)) nrms.append(Vector3.UP) nrms.append(Vector3.UP) nrms.append(Vector3.UP) nrms.append(Vector3.UP) uvs_arr.append(Vector2(float(x) / 8.0, float(z) / 8.0)) uvs_arr.append(Vector2(float(x) / 8.0, float(z + 1) / 8.0)) uvs_arr.append(Vector2(float(x + 1) / 8.0, float(z + 1) / 8.0)) uvs_arr.append(Vector2(float(x + 1) / 8.0, float(z) / 8.0)) indices.append(base) indices.append(base + 1) indices.append(base + 2) indices.append(base) indices.append(base + 2) indices.append(base + 3) if verts.is_empty(): return null var arrays := [] arrays.resize(Mesh.ARRAY_MAX) arrays[Mesh.ARRAY_VERTEX] = verts arrays[Mesh.ARRAY_NORMAL] = nrms arrays[Mesh.ARRAY_TEX_UV] = uvs_arr arrays[Mesh.ARRAY_INDEX] = indices var mesh := ArrayMesh.new() mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) mesh.surface_set_material(0, _build_liquid_material(int(liquid.get("liquid_id", 0)))) var mi := MeshInstance3D.new() mi.mesh = mesh mi.position = chunk_origin - origin_offset mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF mi.name = "Liquid_%d_%d_%d" % [ chunk.get("index_x", 0), chunk.get("index_y", 0), int(liquid.get("liquid_id", 0)), ] return mi static func _build_chunk_material( layers: Array, alpha_maps: Array, tex_names: PackedStringArray, tex_cache: Dictionary, extracted_dir: String, lod: int) -> Material: if lod <= 0: return _build_terrain_material(layers, alpha_maps, tex_names, tex_cache, extracted_dir) var base_tex := _get_layer_texture(0, layers, tex_names, tex_cache, extracted_dir) if base_tex: return _build_single_texture_material(base_tex) return _build_fallback_material() static func _build_liquid_material(liquid_id: int) -> Material: return WOW_LIQUID_MATERIAL.build(liquid_id) static func _build_terrain_material( layers: Array, alpha_maps: Array, tex_names: PackedStringArray, tex_cache: Dictionary, extracted_dir: String) -> Material: if layers.is_empty(): return _build_fallback_material() var textures: Array = [] for li in range(min(layers.size(), 4)): var layer: Dictionary = layers[li] var tex_id: int = layer.get("texture_id", -1) var tex: Texture2D = null if tex_id >= 0 and tex_id < tex_names.size(): tex = _load_texture(tex_names[tex_id], tex_cache, extracted_dir) textures.append(tex) if textures.is_empty() or textures[0] == null: return _build_fallback_material() if textures.size() == 1: return _build_single_texture_material(textures[0]) return _build_layered_material(textures, alpha_maps) static func _get_layer_texture( layer_index: int, layers: Array, tex_names: PackedStringArray, tex_cache: Dictionary, extracted_dir: String) -> Texture2D: if layer_index < 0 or layer_index >= layers.size(): return null var layer: Dictionary = layers[layer_index] var tex_id: int = layer.get("texture_id", -1) if tex_id < 0 or tex_id >= tex_names.size(): return null return _load_texture(tex_names[tex_id], tex_cache, extracted_dir) static func _build_fallback_material() -> StandardMaterial3D: if _fallback_material: return _fallback_material _fallback_material = StandardMaterial3D.new() _fallback_material.roughness = 0.9 _fallback_material.albedo_color = Color(0.4, 0.55, 0.3) _fallback_material.cull_mode = BaseMaterial3D.CULL_DISABLED _fallback_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED return _fallback_material static func _build_baked_texture_material(tex: Texture2D) -> Material: if tex and _baked_texture_material_cache.has(tex.get_instance_id()): return _baked_texture_material_cache[tex.get_instance_id()] var mat := ShaderMaterial.new() mat.shader = _get_single_texture_shader() mat.set_shader_parameter("uv_scale", 1.0) mat.set_shader_parameter("mip_bias", -1.25) mat.set_shader_parameter("sharpen_strength", 0.35) mat.set_shader_parameter("tex0", tex) if tex: _baked_texture_material_cache[tex.get_instance_id()] = mat return mat static func _build_single_texture_material(tex: Texture2D) -> Material: if tex and _single_texture_material_cache.has(tex.get_instance_id()): return _single_texture_material_cache[tex.get_instance_id()] var mat := ShaderMaterial.new() mat.shader = _get_single_texture_shader() mat.set_shader_parameter("uv_scale", 8.0) mat.set_shader_parameter("mip_bias", -0.75) mat.set_shader_parameter("sharpen_strength", 0.0) mat.set_shader_parameter("tex0", tex) if tex: _single_texture_material_cache[tex.get_instance_id()] = mat return mat static func _build_layered_material(textures: Array, alpha_maps: Array) -> ShaderMaterial: var mat := ShaderMaterial.new() mat.shader = _get_terrain_shader() mat.set_shader_parameter("uv_scale", 8.0) mat.set_shader_parameter("mip_bias", -0.75) mat.set_shader_parameter("layer_count", textures.size()) var base_tex: Texture2D = textures[0] for i in range(4): var tex: Texture2D = base_tex if i < textures.size() and textures[i] != null: tex = textures[i] mat.set_shader_parameter("tex%d" % i, tex) for i in range(3): var alpha_tex: Texture2D = _get_empty_alpha_texture() if i < alpha_maps.size(): var built_alpha := _build_alpha_texture(alpha_maps[i]) if built_alpha: alpha_tex = built_alpha mat.set_shader_parameter("alpha%d" % (i + 1), alpha_tex) return mat static func _build_alpha_texture(alpha_bytes: PackedByteArray) -> Texture2D: if alpha_bytes.size() != 64 * 64: return null var image := Image.create_from_data(64, 64, false, Image.FORMAT_L8, alpha_bytes) if image == null: return null return ImageTexture.create_from_image(image) static func _get_empty_alpha_texture() -> Texture2D: if _empty_alpha_texture: return _empty_alpha_texture var bytes := PackedByteArray() bytes.resize(64 * 64) _empty_alpha_texture = ImageTexture.create_from_image( Image.create_from_data(64, 64, false, Image.FORMAT_L8, bytes)) return _empty_alpha_texture static func _build_terrain_texture_images( tex_names: PackedStringArray, image_cache: Dictionary, extracted_dir: String, texture_size: int) -> Array[Image]: if tex_names.is_empty(): var fallback := Image.create(maxi(16, texture_size), maxi(16, texture_size), false, Image.FORMAT_RGBA8) fallback.fill(Color(0.4, 0.55, 0.3, 1.0)) fallback.generate_mipmaps() return [fallback] var target_size := maxi(16, texture_size) var images: Array[Image] = [] for tex_name in tex_names: var source_img := _load_image(tex_name, image_cache, extracted_dir) var img: Image = null if source_img != null: img = source_img.duplicate() else: img = Image.create(target_size, target_size, false, Image.FORMAT_RGBA8) img.fill(Color(0.4, 0.55, 0.3, 1.0)) if img.get_format() != Image.FORMAT_RGBA8: img.convert(Image.FORMAT_RGBA8) if img.get_width() != target_size or img.get_height() != target_size: img.resize(target_size, target_size, Image.INTERPOLATE_LANCZOS) img.generate_mipmaps() images.append(img) return images static func _build_terrain_texture_array_from_images(images: Array) -> Texture2DArray: if images.is_empty(): return null var fallback: Image = null for image_variant in images: var candidate := image_variant as Image if candidate != null and candidate.get_width() > 0 and candidate.get_height() > 0: fallback = candidate break if fallback == null: fallback = Image.create(16, 16, false, Image.FORMAT_RGBA8) fallback.fill(Color(0.4, 0.55, 0.3, 1.0)) fallback.generate_mipmaps() var valid_images: Array[Image] = [] for image_variant in images: var img := image_variant as Image if img != null and img.get_width() > 0 and img.get_height() > 0: valid_images.append(img) continue valid_images.append(fallback) if valid_images.is_empty(): return null var texture_array := Texture2DArray.new() var err := texture_array.create_from_images(valid_images) if err != OK: return null return texture_array static func _build_tile_alpha_atlas(data: Dictionary) -> Texture2D: var atlas_size := 16 * 64 var image := Image.create(atlas_size, atlas_size, false, Image.FORMAT_RGBA8) image.fill(Color(0.0, 0.0, 0.0, 1.0)) var tile_origin := get_tile_origin(data) for chunk in data.get("chunks", []) as Array: if chunk.is_empty(): continue var chunk_coord := _get_chunk_grid_coord(chunk, tile_origin) var base_x := chunk_coord.x * 64 var base_y := chunk_coord.y * 64 var alpha_maps: Array = chunk.get("alpha_maps", []) for py in range(64): for px in range(64): var a0 := 0 var a1 := 0 var a2 := 0 if alpha_maps.size() > 0: var bytes0: PackedByteArray = alpha_maps[0] if bytes0.size() == 64 * 64: a0 = bytes0[py * 64 + px] if alpha_maps.size() > 1: var bytes1: PackedByteArray = alpha_maps[1] if bytes1.size() == 64 * 64: a1 = bytes1[py * 64 + px] if alpha_maps.size() > 2: var bytes2: PackedByteArray = alpha_maps[2] if bytes2.size() == 64 * 64: a2 = bytes2[py * 64 + px] image.set_pixel( base_x + px, base_y + py, Color8(a0, a1, a2, 255)) return ImageTexture.create_from_image(image) static func _build_tile_layer_index_map(data: Dictionary, texture_count: int) -> Texture2D: var image := Image.create(16, 16, false, Image.FORMAT_RGBA8) image.fill(Color8(0, 0, 0, 255)) var tile_origin := get_tile_origin(data) var max_id := mini(maxi(texture_count - 1, 0), 255) for chunk in data.get("chunks", []) as Array: if chunk.is_empty(): continue var layers: Array = chunk.get("layers", []) if layers.is_empty(): continue var ids := PackedByteArray([0, 0, 0, 0]) for i in range(mini(4, layers.size())): var layer: Dictionary = layers[i] ids[i] = clampi(int(layer.get("texture_id", 0)), 0, max_id) var chunk_coord := _get_chunk_grid_coord(chunk, tile_origin) image.set_pixel( chunk_coord.x, chunk_coord.y, Color8(ids[0], ids[1], ids[2], ids[3])) return ImageTexture.create_from_image(image) static func _get_chunk_layer_color(chunk: Dictionary, max_id: int) -> Color: var layers: Array = chunk.get("layers", []) if layers.is_empty(): return Color(0.0, 0.0, 0.0, 0.0) var ids := PackedFloat32Array([0.0, 0.0, 0.0, 0.0]) for i in range(mini(4, layers.size())): var layer: Dictionary = layers[i] ids[i] = float(clampi(int(layer.get("texture_id", 0)), 0, max_id)) / 255.0 return Color(ids[0], ids[1], ids[2], ids[3]) static func _get_chunk_grid_coord(chunk: Dictionary, tile_origin: Vector3) -> Vector2i: var origin: Vector3 = chunk.get("origin", Vector3.ZERO) var local := origin - tile_origin return Vector2i( clampi(int(round(local.x / CHUNK_SIZE)), 0, 15), clampi(int(round(local.z / CHUNK_SIZE)), 0, 15)) static func _build_control_splat_material( texture_array: Texture2DArray, alpha_atlas: Texture2D, layer_index_map: Texture2D) -> ShaderMaterial: var mat := ShaderMaterial.new() mat.shader = _get_control_splat_shader() mat.set_shader_parameter("terrain_tex", texture_array) mat.set_shader_parameter("alpha_atlas", alpha_atlas) mat.set_shader_parameter("layer_index_map", layer_index_map) mat.set_shader_parameter("uv_scale", 8.0) mat.set_shader_parameter("mip_bias", -0.75) return mat static func _get_control_splat_shader() -> Shader: if _control_splat_shader: return _control_splat_shader _control_splat_shader = Shader.new() _control_splat_shader.code = """ shader_type spatial; render_mode unshaded, cull_disabled, depth_draw_opaque; uniform sampler2DArray terrain_tex : source_color, filter_linear_mipmap_anisotropic, repeat_enable; uniform sampler2D alpha_atlas : filter_linear, repeat_disable; uniform sampler2D layer_index_map : filter_nearest, repeat_disable; uniform float uv_scale = 8.0; uniform float mip_bias = -0.75; uniform vec3 light_dir = vec3(-0.35, 0.82, -0.45); uniform float ambient = 0.62; uniform float diffuse = 0.38; uniform float hemi = 0.12; vec3 sample_layer(float layer, vec2 continuous_chunk_uv) { return texture(terrain_tex, vec3(continuous_chunk_uv * uv_scale, layer), mip_bias).rgb; } void fragment() { vec2 tile_uv = clamp(UV, vec2(0.0), vec2(0.999999)); vec2 chunk_uv = tile_uv * 16.0; vec2 chunk_f = floor(chunk_uv); vec2 local_uv = chunk_uv - chunk_f; vec4 packed_ids = COLOR; vec4 layers = floor(packed_ids * 255.0 + vec4(0.5)); vec2 alpha_px = clamp(local_uv * 64.0, vec2(0.5), vec2(63.5)); vec3 alpha = texture(alpha_atlas, (chunk_f * 64.0 + alpha_px) / 1024.0).rgb; float w1 = alpha.r; float w2 = alpha.g; float w3 = alpha.b; float w0 = max(0.0, 1.0 - w1 - w2 - w3); float sum = max(w0 + w1 + w2 + w3, 0.0001); w0 /= sum; w1 /= sum; w2 /= sum; w3 /= sum; vec3 albedo = sample_layer(layers.x, chunk_uv) * w0 + sample_layer(layers.y, chunk_uv) * w1 + sample_layer(layers.z, chunk_uv) * w2 + sample_layer(layers.w, chunk_uv) * w3; vec3 n = normalize(NORMAL); if (!FRONT_FACING) { n = -n; } vec3 l = normalize(light_dir); float ndl = max(dot(n, l), 0.0); float hemi_term = clamp(n.y * 0.5 + 0.5, 0.0, 1.0); float light = ambient + diffuse * ndl + hemi * hemi_term; ALBEDO = albedo * light; } """ return _control_splat_shader static func _build_tile_baked_albedo( data: Dictionary, image_cache: Dictionary, extracted_dir: String, texture_size: int) -> Image: var native_image := _build_tile_baked_albedo_native( data, image_cache, extracted_dir, texture_size) if native_image != null: return native_image var chunk_pixels := int(_get_baked_albedo_size(texture_size) / 16) var size := chunk_pixels * 16 var image := Image.create(size, size, false, Image.FORMAT_RGB8) image.fill(Color(0.4, 0.55, 0.3)) var tex_names: PackedStringArray = data.get("textures", PackedStringArray()) for chunk in data.get("chunks", []) as Array: if chunk.is_empty(): continue var chunk_x: int = chunk.get("index_x", -1) var chunk_y: int = chunk.get("index_y", -1) if chunk_x < 0 or chunk_x >= 16 or chunk_y < 0 or chunk_y >= 16: continue var base_x := chunk_x * chunk_pixels var base_y := chunk_y * chunk_pixels for py in range(chunk_pixels): var local_v := (float(py) + 0.5) / float(chunk_pixels) for px in range(chunk_pixels): var local_u := (float(px) + 0.5) / float(chunk_pixels) var color := _sample_chunk_baked_albedo( chunk, tex_names, image_cache, extracted_dir, local_u, local_v) image.set_pixel(base_x + px, base_y + py, color) return image static func _build_tile_baked_albedo_native( data: Dictionary, image_cache: Dictionary, extracted_dir: String, texture_size: int) -> Image: if not ClassDB.class_exists("ADTBaker"): return null if _native_adt_baker == null: _native_adt_baker = ClassDB.instantiate("ADTBaker") if _native_adt_baker == null: return null return _native_adt_baker.call( "bake_tile_albedo", data, image_cache, extracted_dir, texture_size) as Image static func _get_baked_albedo_size(texture_size: int) -> int: return maxi(8, int(ceil(float(maxi(texture_size, 16)) / 16.0))) * 16 static func _sample_chunk_baked_albedo( chunk: Dictionary, tex_names: PackedStringArray, image_cache: Dictionary, extracted_dir: String, local_u: float, local_v: float) -> Color: var layers: Array = chunk.get("layers", []) if layers.is_empty(): return Color(0.4, 0.55, 0.3) var textures: Array = [] for li in range(min(layers.size(), 4)): var layer: Dictionary = layers[li] var tex_id: int = layer.get("texture_id", -1) var img: Image = null if tex_id >= 0 and tex_id < tex_names.size(): img = _load_image(tex_names[tex_id], image_cache, extracted_dir) textures.append(img) if textures.is_empty() or textures[0] == null: return Color(0.4, 0.55, 0.3) var alpha_maps: Array = chunk.get("alpha_maps", []) var weights := PackedFloat32Array([1.0, 0.0, 0.0, 0.0]) for li in range(1, min(layers.size(), 4)): var alpha: PackedByteArray = alpha_maps[li - 1] if li - 1 < alpha_maps.size() else PackedByteArray() if alpha.size() == 64 * 64: var ax := clampi(int(floor(local_u * 63.999)), 0, 63) var ay := clampi(int(floor(local_v * 63.999)), 0, 63) weights[li] = float(alpha[ay * 64 + ax]) / 255.0 weights[0] = maxf(0.0, 1.0 - weights[1] - weights[2] - weights[3]) var sum: float = weights[0] + weights[1] + weights[2] + weights[3] if sum > 0.0: for i in range(4): weights[i] /= sum var tiled_u := local_u * 8.0 var tiled_v := local_v * 8.0 var out := Color(0, 0, 0, 1) for i in range(min(textures.size(), 4)): var img: Image = textures[i] if img == null: continue out += _sample_image_repeat(img, tiled_u, tiled_v) * weights[i] return Color(out.r, out.g, out.b, 1.0) static func _sample_image_repeat(image: Image, u: float, v: float) -> Color: var width: int = image.get_width() var height: int = image.get_height() if width <= 0 or height <= 0: return Color(1, 1, 1, 1) var fu: float = wrapf(u, 0.0, 1.0) var fv: float = wrapf(v, 0.0, 1.0) var px: float = fu * float(width) - 0.5 var py: float = fv * float(height) - 0.5 var x0: int = posmod(int(floor(px)), width) var y0: int = posmod(int(floor(py)), height) var x1: int = posmod(x0 + 1, width) var y1: int = posmod(y0 + 1, height) var tx: float = px - floor(px) var ty: float = py - floor(py) var c00: Color = image.get_pixel(x0, y0) var c10: Color = image.get_pixel(x1, y0) var c01: Color = image.get_pixel(x0, y1) var c11: Color = image.get_pixel(x1, y1) var top: Color = c00.lerp(c10, tx) var bottom: Color = c01.lerp(c11, tx) return top.lerp(bottom, ty) static func _get_terrain_shader() -> Shader: if _terrain_shader: return _terrain_shader _terrain_shader = Shader.new() _terrain_shader.code = """ shader_type spatial; render_mode unshaded, cull_disabled, depth_draw_opaque; uniform sampler2D tex0 : source_color, filter_linear_mipmap_anisotropic, repeat_enable; uniform sampler2D tex1 : source_color, filter_linear_mipmap_anisotropic, repeat_enable; uniform sampler2D tex2 : source_color, filter_linear_mipmap_anisotropic, repeat_enable; uniform sampler2D tex3 : source_color, filter_linear_mipmap_anisotropic, repeat_enable; uniform sampler2D alpha1 : filter_linear, repeat_disable; uniform sampler2D alpha2 : filter_linear, repeat_disable; uniform sampler2D alpha3 : filter_linear, repeat_disable; uniform int layer_count = 1; uniform float uv_scale = 8.0; uniform float mip_bias = -0.75; uniform vec3 light_dir = vec3(-0.35, 0.82, -0.45); uniform float ambient = 0.62; uniform float diffuse = 0.38; uniform float hemi = 0.12; void fragment() { vec2 tiled_uv = UV * uv_scale; vec4 c0 = texture(tex0, tiled_uv, mip_bias); vec4 c1 = texture(tex1, tiled_uv, mip_bias); vec4 c2 = texture(tex2, tiled_uv, mip_bias); vec4 c3 = texture(tex3, tiled_uv, mip_bias); float w1 = layer_count > 1 ? texture(alpha1, UV).r : 0.0; float w2 = layer_count > 2 ? texture(alpha2, UV).r : 0.0; float w3 = layer_count > 3 ? texture(alpha3, UV).r : 0.0; float w0 = max(0.0, 1.0 - w1 - w2 - w3); float sum = w0; if (layer_count > 1) { sum += w1; } if (layer_count > 2) { sum += w2; } if (layer_count > 3) { sum += w3; } if (sum > 0.0) { w0 /= sum; w1 /= sum; w2 /= sum; w3 /= sum; } vec4 albedo = c0 * w0; if (layer_count > 1) { albedo += c1 * w1; } if (layer_count > 2) { albedo += c2 * w2; } if (layer_count > 3) { albedo += c3 * w3; } vec3 n = normalize(NORMAL); if (!FRONT_FACING) { n = -n; } vec3 l = normalize(light_dir); float ndl = max(dot(n, l), 0.0); float hemi_term = clamp(n.y * 0.5 + 0.5, 0.0, 1.0); float light = ambient + diffuse * ndl + hemi * hemi_term; ALBEDO = albedo.rgb * light; } """ return _terrain_shader static func _get_single_texture_shader() -> Shader: if _single_texture_shader: return _single_texture_shader _single_texture_shader = Shader.new() _single_texture_shader.code = """ shader_type spatial; render_mode unshaded, cull_disabled, depth_draw_opaque; uniform sampler2D tex0 : source_color, filter_linear_mipmap_anisotropic, repeat_enable; uniform float uv_scale = 8.0; uniform float mip_bias = -0.75; uniform float sharpen_strength = 0.0; uniform vec3 light_dir = vec3(-0.35, 0.82, -0.45); uniform float ambient = 0.62; uniform float diffuse = 0.38; uniform float hemi = 0.12; vec3 sample_sharp(sampler2D tex, vec2 uv) { vec3 center = texture(tex, uv, mip_bias).rgb; if (sharpen_strength <= 0.001) { return center; } vec2 texel = 1.0 / vec2(textureSize(tex, 0)); vec2 min_uv = vec2(0.001); vec2 max_uv = vec2(0.999); vec3 side = texture(tex, clamp(uv + vec2(texel.x, 0.0), min_uv, max_uv), mip_bias).rgb + texture(tex, clamp(uv - vec2(texel.x, 0.0), min_uv, max_uv), mip_bias).rgb + texture(tex, clamp(uv + vec2(0.0, texel.y), min_uv, max_uv), mip_bias).rgb + texture(tex, clamp(uv - vec2(0.0, texel.y), min_uv, max_uv), mip_bias).rgb; return clamp(center * (1.0 + 4.0 * sharpen_strength) - side * sharpen_strength, vec3(0.0), vec3(1.0)); } void fragment() { vec3 n = normalize(NORMAL); if (!FRONT_FACING) { n = -n; } vec3 l = normalize(light_dir); float ndl = max(dot(n, l), 0.0); float hemi_term = clamp(n.y * 0.5 + 0.5, 0.0, 1.0); float light = ambient + diffuse * ndl + hemi * hemi_term; ALBEDO = sample_sharp(tex0, UV * uv_scale) * light; } """ return _single_texture_shader static func _load_texture( blp_path: String, cache: Dictionary, extracted_dir: String) -> Texture2D: if cache.has(blp_path): return cache[blp_path] if not ClassDB.class_exists("BLPLoader"): return null var abs_path := extracted_dir.path_join(blp_path.replace("\\", "/")) if not FileAccess.file_exists(abs_path): cache[blp_path] = null return null var img: Image = ClassDB.instantiate("BLPLoader").call("load_image", abs_path) if not img: cache[blp_path] = null return null img.generate_mipmaps() var tex := ImageTexture.create_from_image(img) cache[blp_path] = tex return tex static func _load_image( blp_path: String, cache: Dictionary, extracted_dir: String) -> Image: if cache.has(blp_path): return cache[blp_path] if not ClassDB.class_exists("BLPLoader"): return null var abs_path := extracted_dir.path_join(blp_path.replace("\\", "/")) if not FileAccess.file_exists(abs_path): cache[blp_path] = null return null var img: Image = ClassDB.instantiate("BLPLoader").call("load_image", abs_path) if not img: cache[blp_path] = null return null cache[blp_path] = img return img