1141 lines
33 KiB
GDScript
1141 lines
33 KiB
GDScript
## Converts raw ADTLoader data into a Godot Node3D terrain scene.
|
|
extends RefCounted
|
|
class_name ADTBuilder
|
|
|
|
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 _single_texture_shader: Shader
|
|
static var _empty_alpha_texture: Texture2D
|
|
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 var _liquid_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 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 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)
|
|
|
|
var baked_image := _build_tile_baked_albedo(data, image_cache, extracted_dir, texture_size)
|
|
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())
|
|
|
|
return {
|
|
"mesh": mesh,
|
|
"position": Vector3.ZERO,
|
|
"name": "TileLOD%d" % lod,
|
|
}
|
|
|
|
|
|
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)
|
|
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
|
|
|
|
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(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)
|
|
|
|
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.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) -> StandardMaterial3D:
|
|
if _liquid_material_cache.has(liquid_id):
|
|
return _liquid_material_cache[liquid_id]
|
|
|
|
var color := _get_liquid_color(liquid_id)
|
|
var mat := StandardMaterial3D.new()
|
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
mat.albedo_color = color
|
|
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
|
mat.roughness = 0.08
|
|
mat.metallic = 0.0
|
|
mat.emission_enabled = true
|
|
mat.emission = Color(color.r, color.g, color.b) * 0.08
|
|
_liquid_material_cache[liquid_id] = mat
|
|
return mat
|
|
|
|
|
|
static func _get_liquid_color(liquid_id: int) -> Color:
|
|
match liquid_id:
|
|
2, 14:
|
|
return Color(0.08, 0.20, 0.34, 0.62)
|
|
3, 15, 19:
|
|
return Color(0.75, 0.28, 0.03, 0.72)
|
|
4, 20:
|
|
return Color(0.24, 0.45, 0.14, 0.68)
|
|
_:
|
|
return Color(0.12, 0.33, 0.50, 0.58)
|
|
|
|
|
|
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("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("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("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_tile_baked_albedo(
|
|
data: Dictionary,
|
|
image_cache: Dictionary,
|
|
extracted_dir: String,
|
|
texture_size: int) -> Image:
|
|
|
|
var chunk_pixels := maxi(8, int(ceil(float(maxi(texture_size, 16)) / 16.0)))
|
|
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 _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 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);
|
|
vec4 c1 = texture(tex1, tiled_uv);
|
|
vec4 c2 = texture(tex2, tiled_uv);
|
|
vec4 c3 = texture(tex3, tiled_uv);
|
|
|
|
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 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() {
|
|
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 = texture(tex0, UV * uv_scale).rgb * 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
|
|
|
|
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
|