## The pack-textured material of ONE DCLevel (hyper-games #94 P1 step 3, design §8).
##
## Owns the ShaderMaterial on the level mesh and keeps it in step with the brushes:
## when DCLevel's derived projection table changes (its version bumps after any
## edit that touches face textures), sync() collects the texture ids the live
## brushes name, loads any not yet in the arrays, hands DCLevel the id -> layer map,
## and uploads the table.
##
## GROW-ONLY: an id stays loaded until the map unloads, so earlier ids keep their
## layer and a rebuild only APPENDS. A texture that falls out of use costs its layer
## until the next map load; that is cheaper than re-uploading every array on undo.
##
## Two rebuilds for a newly named texture, by construction: the edit's own rebuild
## derives the unknown id onto the missing layer and bumps the version; sync() loads
## it, calls set_texture_info and rebuilds once more so the rows carry the real layer.
class_name LevelPackMaterial
extends RefCounted
const SHADER_PATH := "res://scenes/maps/dc_level.gdshader"
## Surface detail tiers (Settings "surface_detail") — COMPILED variants of one body
## (dc_level.gdshaderinc), because a uniform-switched POM loop measured a cost even
## when switched off. 0 = Normal maps, 1 = Parallax.
const SHADERS: Array[String] = [SHADER_PATH, "res://scenes/maps/dc_level_pom.gdshader"]
## What a face with no texture intent draws: the grey the empty level always had.
const PLAIN := Color(0.62, 0.6, 0.56)
var level: Node = null
var mat: ShaderMaterial = null
var _res := 1024
var _detail := 0
var _params: Dictionary = {} # every parameter set, re-applied after a tier swap
var _built: Dictionary = {}
var _loaded := PackedStringArray()
var _table_version := -1
var _arrays_dirty := true
## Wear the pack material. False when the loaded dc_terrain build predates the P1
## texture API (an old .so) or the shader is missing — the caller keeps its plain
## material then, rather than drawing every face as a debug colour.
func install(p_level: Node, mi: MeshInstance3D, res: int, detail: int = 0) -> bool:
if p_level == null or mi == null or not p_level.has_method("set_texture_info"):
return false
_detail = clampi(detail, 0, SHADERS.size() - 1)
var shader := load(SHADERS[_detail]) as Shader
if shader == null:
push_warning("[LevelPackMaterial] %s failed to load" % SHADERS[_detail])
return false
level = p_level
_res = res
mat = ShaderMaterial.new()
mat.shader = shader
_param("use_textures", true)
_param("use_pbr", true)
_param("use_orm", true)
_param("none_plain", true)
_param("none_color", PLAIN)
mi.material_override = mat
wear(p_level, mat)
sync()
return true
## Put the level material on EVERYTHING the level draws: LevelMesh and the carved
## LevelPockets (dc_level.cpp names the pocket node so this walk finds it; it uses
## the same vertex encoding, so the same shader is correct). Every path that sets
## the level's material goes through here.
static func wear(p_level: Node, m: Material) -> void:
if p_level == null:
return
for n: String in ["LevelMesh", "LevelPockets"]:
var mi := p_level.get_node_or_null(n) as MeshInstance3D
if mi != null:
mi.material_override = m
## Switch the surface-detail tier: a different COMPILED shader on the same material
## (so LevelMesh and LevelPockets follow), every parameter re-applied.
func set_detail(detail: int) -> void:
detail = clampi(detail, 0, SHADERS.size() - 1)
if mat == null or detail == _detail:
return
var shader := load(SHADERS[detail]) as Shader
if shader == null:
push_warning("[LevelPackMaterial] %s failed to load — tier unchanged" % SHADERS[detail])
return
_detail = detail
mat.shader = shader
for k: String in _params:
mat.set_shader_parameter(k, _params[k])
func _param(param: String, value: Variant) -> void:
_params[param] = value
mat.set_shader_parameter(param, value)
func set_res(res: int) -> void:
if res != _res:
_res = res
_arrays_dirty = true
## id -> {layer, tile_u, tile_v} of what is loaded now.
func info() -> Dictionary:
return _built.get("info", {})
## Cheap when nothing changed: one int compare per call.
func sync() -> void:
if level == null or mat == null:
return
var v: int = level.get_projection_table_version()
if v == _table_version and not _arrays_dirty:
return
_table_version = v
var grew := false
for id in _used_ids():
if not _loaded.has(id) and TexturePacks.has(id):
_loaded.append(id)
grew = true
if grew or _arrays_dirty:
_arrays_dirty = false
_built = TexturePacks.build(_loaded, _res)
_param("tex_atlas", _built["albedo"])
_param("norm_atlas", _built["norm"])
_param("orm_atlas", _built["orm"])
_upload_layer_params()
print("[LevelPackMaterial] %d texture(s) at %d px, %d unknown" % [
_loaded.size(), _res, (_built["unknown"] as PackedStringArray).size()])
if grew:
# New ids have layers and tile sizes now; DCLevel only re-derives in
# rebuild() (nothing consumes its dirty flag), so do it here. A resolution
# change alone leaves layers and tiles as they were — no rebuild for that.
level.set_texture_info(_built["info"], int(_built["missing_layer"]))
level.rebuild()
_table_version = level.get_projection_table_version()
_upload_table(level.get_projection_table())
func _used_ids() -> PackedStringArray:
var out := PackedStringArray()
for id: int in level.get_live_brush_ids():
for t: Dictionary in level.get_brush(id).get("face_tex", []):
var tex := String(t.get("tex", ""))
if tex != "" and int(t.get("mode", 0)) != 0 and not out.has(tex):
out.append(tex)
return out
## Same layout map_dc_level._apply_textures uploads: 4 RGBAF texels per row.
func _upload_table(tab: PackedFloat32Array) -> void:
var rows := tab.size() / 16
_param("has_projection_table", rows > 0)
_param("projection_rows", rows)
if rows > 0:
var img := Image.create_from_data(4, rows, false, Image.FORMAT_RGBAF, tab.to_byte_array())
_param("projection_table", ImageTexture.create_from_image(img))
## One texel per array layer: R = relief depth in metres (pack.json height_m, 0 = flat),
## read by the Parallax tier at the face's layer. Layer 0 (missing) stays 0.
func _upload_layer_params() -> void:
var n := 1
var info_d: Dictionary = _built.get("info", {})
for id: String in info_d:
n = maxi(n, int(info_d[id]["layer"]) + 1)
var img := Image.create(n, 1, false, Image.FORMAT_RF)
for id: String in info_d:
img.set_pixel(int(info_d[id]["layer"]), 0, Color(float(info_d[id].get("height_m", 0.0)), 0, 0))
_param("layer_params", ImageTexture.create_from_image(img))