OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / maps / level_pack_material.gd

172 lines · gd ref:
## 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))