Files
sindoring 09b3e87a8d fix(M00): release GUI capture render resources
Work-Package: M00-QAR-GUI-RID-SHUTDOWN-001
Agent: sindo-main-codex
Tests: render baseline dry-run; runtime cache shutdown; comparator self-test; coordination and documentation gates
Fidelity: shutdown-only ownership fix; no visual or Blizzlike335 behavior change
2026-07-13 11:08:30 +04:00

1723 lines
51 KiB
GDScript

## 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 = {}
static func _ensure_wow_shader_globals() -> void:
pass
## 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)
if root.get_child_count() > 0:
return root
root.free()
return 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
_ensure_wow_shader_globals()
_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;
global uniform vec4 wow_ambient_color;
global uniform vec4 wow_light_color;
global uniform vec3 wow_light_dir;
global uniform vec4 wow_fog_color;
global uniform vec2 wow_fog_range;
global uniform float wow_fog_density;
varying vec3 world_pos;
varying vec3 world_normal;
varying vec3 view_pos;
void vertex() {
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
world_normal = normalize(MODEL_NORMAL_MATRIX * NORMAL);
view_pos = (MODELVIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
}
vec3 sample_layer(float layer, vec2 continuous_chunk_uv) {
return texture(terrain_tex, vec3(continuous_chunk_uv * uv_scale, layer), mip_bias).rgb;
}
vec3 apply_wow_fog(vec3 color) {
float dist = length(view_pos);
float range_fog = smoothstep(wow_fog_range.x, wow_fog_range.y, dist);
float fog_amount = range_fog * clamp(wow_fog_density, 0.0, 0.55);
return mix(color, wow_fog_color.rgb, fog_amount);
}
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(world_normal);
if (!FRONT_FACING) {
n = -n;
}
vec3 l = normalize(wow_light_dir);
float ndl = max(dot(n, l), 0.0);
float hemi_term = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
vec3 ambient_tint = mix(vec3(0.74), max(wow_ambient_color.rgb, vec3(0.08)), 0.22);
vec3 light_tint = mix(vec3(1.0), wow_light_color.rgb, 0.12);
vec3 light = ambient_tint * ambient + light_tint * diffuse * ndl + vec3(hemi * hemi_term);
light = clamp(light, vec3(0.48), vec3(1.08));
ALBEDO = apply_wow_fog(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
_ensure_wow_shader_globals()
_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;
global uniform vec4 wow_ambient_color;
global uniform vec4 wow_light_color;
global uniform vec3 wow_light_dir;
global uniform vec4 wow_fog_color;
global uniform vec2 wow_fog_range;
global uniform float wow_fog_density;
varying vec3 world_pos;
varying vec3 world_normal;
varying vec3 view_pos;
void vertex() {
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
world_normal = normalize(MODEL_NORMAL_MATRIX * NORMAL);
view_pos = (MODELVIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
}
vec3 apply_wow_fog(vec3 color) {
float dist = length(view_pos);
float range_fog = smoothstep(wow_fog_range.x, wow_fog_range.y, dist);
float fog_amount = range_fog * clamp(wow_fog_density, 0.0, 0.55);
return mix(color, wow_fog_color.rgb, fog_amount);
}
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(world_normal);
if (!FRONT_FACING) {
n = -n;
}
vec3 l = normalize(wow_light_dir);
float ndl = max(dot(n, l), 0.0);
float hemi_term = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
vec3 ambient_tint = mix(vec3(0.74), max(wow_ambient_color.rgb, vec3(0.08)), 0.22);
vec3 light_tint = mix(vec3(1.0), wow_light_color.rgb, 0.12);
vec3 light = ambient_tint * ambient + light_tint * diffuse * ndl + vec3(hemi * hemi_term);
light = clamp(light, vec3(0.48), vec3(1.08));
ALBEDO = apply_wow_fog(albedo.rgb * light);
}
"""
return _terrain_shader
static func _get_single_texture_shader() -> Shader:
if _single_texture_shader:
return _single_texture_shader
_ensure_wow_shader_globals()
_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;
global uniform vec4 wow_ambient_color;
global uniform vec4 wow_light_color;
global uniform vec3 wow_light_dir;
global uniform vec4 wow_fog_color;
global uniform vec2 wow_fog_range;
global uniform float wow_fog_density;
varying vec3 world_pos;
varying vec3 world_normal;
varying vec3 view_pos;
void vertex() {
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
world_normal = normalize(MODEL_NORMAL_MATRIX * NORMAL);
view_pos = (MODELVIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
}
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));
}
vec3 apply_wow_fog(vec3 color) {
float dist = length(view_pos);
float range_fog = smoothstep(wow_fog_range.x, wow_fog_range.y, dist);
float fog_amount = range_fog * clamp(wow_fog_density, 0.0, 0.55);
return mix(color, wow_fog_color.rgb, fog_amount);
}
void fragment() {
vec3 n = normalize(world_normal);
if (!FRONT_FACING) {
n = -n;
}
vec3 l = normalize(wow_light_dir);
float ndl = max(dot(n, l), 0.0);
float hemi_term = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
vec3 ambient_tint = mix(vec3(0.74), max(wow_ambient_color.rgb, vec3(0.08)), 0.22);
vec3 light_tint = mix(vec3(1.0), wow_light_color.rgb, 0.12);
vec3 light = ambient_tint * ambient + light_tint * diffuse * ndl + vec3(hemi * hemi_term);
light = clamp(light, vec3(0.48), vec3(1.08));
ALBEDO = apply_wow_fog(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