From b100a5bc51039bfc0ceae849c0f93f0f5dc63d0b Mon Sep 17 00:00:00 2001 From: enne2 Date: Sun, 11 Oct 2026 01:10:39 +0200 Subject: [PATCH] Align original Half-Life office walls and ceiling with explicit room UVs --- assets/textures/office/ceiling.png | Bin 0 -> 2618 bytes assets/textures/office/ceiling.png.import | 40 +++++++++++++ assets/textures/office/source_manifest.json | 21 +++++++ assets/textures/office/wall.png | Bin 0 -> 15999 bytes assets/textures/office/wall.png.import | 40 +++++++++++++ docs/OFFICE-SURFACES.md | 57 +++++++++++++++++++ docs/WEB-DEPLOY.md | 9 +++ scripts/room.gd | 50 ++++++++++++++-- tools/extract_office_surfaces.py | 36 ++++++++++++ tools/test_office_uv.gd | 60 ++++++++++++++++++++ tools/test_office_uv.gd.uid | 1 + 11 files changed, 309 insertions(+), 5 deletions(-) create mode 100644 assets/textures/office/ceiling.png create mode 100644 assets/textures/office/ceiling.png.import create mode 100644 assets/textures/office/source_manifest.json create mode 100644 assets/textures/office/wall.png create mode 100644 assets/textures/office/wall.png.import create mode 100644 docs/OFFICE-SURFACES.md create mode 100644 tools/extract_office_surfaces.py create mode 100644 tools/test_office_uv.gd create mode 100644 tools/test_office_uv.gd.uid diff --git a/assets/textures/office/ceiling.png b/assets/textures/office/ceiling.png new file mode 100644 index 0000000000000000000000000000000000000000..a1998cc5844db61cab9472845f8ae4d3137bd377 GIT binary patch literal 2618 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 cJsF7q0dtl-UBc;=U)Nn1%57o_46?>-f-Q0 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 literal 0 HcmV?d00001 diff --git a/assets/textures/office/wall.png.import b/assets/textures/office/wall.png.import new file mode 100644 index 0000000..e496c34 --- /dev/null +++ b/assets/textures/office/wall.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://5o7es0d7q52c" +path="res://.godot/imported/wall.png-3df74578c1b45f448ae6dfd2af7485e0.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/textures/office/wall.png" +dest_files=["res://.godot/imported/wall.png-3df74578c1b45f448ae6dfd2af7485e0.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/OFFICE-SURFACES.md b/docs/OFFICE-SURFACES.md new file mode 100644 index 0000000..4133899 --- /dev/null +++ b/docs/OFFICE-SURFACES.md @@ -0,0 +1,57 @@ +# Aligned Half-Life office walls and ceiling + +## Research and asset choice + +Perplexity was unavailable. Exa found the community VERC guide and original-game +material catalog; actual candidates were then inspected from the locally installed +Half-Life `halflife.wad` (not textures from the Black Mesa remake). + +- https://twhl.info/index.php/wiki/page/VERC%3A_Style_Guide_1 +- https://greg-kennedy.com/hl_materials/ +- https://twhl.info/index.php/vault/view/6388 + +The VERC guide identifies green `C1A0_LABW*` wall panels and `C1A0_C1` ceiling in +the early laboratory aesthetic. For the furnished OFFICE room we chose the original +FIFTIES family instead: `FIFTIES_WALL14U` (128x160, plaster above wood paneling) and +`FIFTIES_CEIL5` (48x48, four square acoustic panels). The guide is a community +reference, not proof that this exact pair appeared in a specific original office. +Candidates were compared visually before selection. Original pixels are preserved. + +`tools/extract_office_surfaces.py` exports just these two textures to +`assets/textures/office/`, plus source WAD and texture-lump SHA256 manifest. It +never changes Steam files or re-extracts zombie assets. + +```sh +python3 tools/extract_office_surfaces.py /path/to/Half-Life/valve/halflife.wad +``` + +## UV layout + +The old generic world-space triplanar mapping did not distinguish wall motifs from +ceiling motifs. `room.gd` now retains BoxMesh vertices/normals/indices but rebuilds +wall/ceiling meshes as ArrayMesh with explicit room-space UVs. Existing collision +boxes and placement remain untouched. + +- Walls: U advances clockwise along the room perimeter at one texture per 4m. + V runs floor-to-ceiling once, inverted so the wood is BELOW the plaster. Equal + density128px/4m =160px/5m, no aspect distortion or repeated vertical trim bands. + All window segments share the same coordinate origin; segment size does not + reset the mapping. The 72m perimeter is18 full repeats, so all corners close. + V uses a half-texel inset at the floor/ceiling to avoid exact0/1 wrap-edge sampling. +- Ceiling: XZ planar UVs with a shared origin at(-9,-9). One48x48 image covers1.2m + and contains two panels per axis, so each panel is0.6m. The18m ceiling spans15x15 + texture repeats (30x30 panels). No per-face rotations or arbitrary local scaling. +- Linear mipmapped anisotropic filtering and U tiling remain enabled. +- Floor keeps its existing texture/projection. Furniture, lighting and gameplay + are unchanged. The old wall/ceiling PNGs are retained but no longer used here. + +## Verification + +`tools/test_office_uv.gd`: seventeen explicit-UV surfaces, no triplanar on office +materials, shared UVs across window segments,4m wall period, wall corner phase +continuity/full-perimeter closure, and ceiling15x15 tiling. PASS headless and real +GL Compatibility. Images `/tmp/zombie-office-uv-{0,1,2}.png` inspect opposite walls +and ceiling; first two use a TEMPORARY inspection light, last uses actual gameplay +lighting. The inspection light exists only in the test, not in the game. + +Existing resource-in-use warnings at test shutdown remain. diff --git a/docs/WEB-DEPLOY.md b/docs/WEB-DEPLOY.md index f631d8d..5a23294 100644 --- a/docs/WEB-DEPLOY.md +++ b/docs/WEB-DEPLOY.md @@ -1,5 +1,14 @@ # Web build and compressed deployment +## Aligned office surfaces release: 9a376a3dbf674069 + +See `docs/OFFICE-SURFACES.md`: original FIFTIES_WALL14U and FIFTIES_CEIL5; +explicit continuous wall/perimeter UVs and regular XZ ceiling panels. +PCK 3,989,720 bytes; gzip payload total 12,440,335 bytes. Previous releases retained. +Root backup `index-before-office-uv.html`; serial staging import retained. +Disk before upload: about1.1GB free (98% used); old-release cleanup needs approval. + + ## Gameplay improvements release: 7ebf03fe3c88be15 See `docs/GAMEPLAY-IMPROVEMENTS.md`: amber HUD, automatic reload, shared furniture diff --git a/scripts/room.gd b/scripts/room.gd index 66c0028..18a4372 100644 --- a/scripts/room.gd +++ b/scripts/room.gd @@ -7,6 +7,8 @@ const W: float = 18.0 # larghezza (x) const D: float = 18.0 # profondita (z) const H: float = 5.0 # altezza (y) const WALL_T: float = 0.3 +const WALL_REPEAT_M := 4.0 # 128x160 texture spans 4x5m: equal texel density +const CEILING_REPEAT_M := 1.2 # CEIL5 contains 2x2 panels: each panel = 0.6m # Finestra: 4 m x 2 m, bordo basso a 1.5 m const WIN_W: float = 4.0 @@ -167,7 +169,22 @@ func _add_box_to(parent: Node3D, s: Vector3, c: Vector3, m: StandardMaterial3D) var bm := BoxMesh.new() bm.size = s bm.material = m - mi.mesh = bm + if m.has_meta("office_surface"): + # BoxMesh's per-face UVs reset on every wall segment. Rebuild only UVs; + # geometry, normals, indices and collision boxes remain identical. + var arrays := bm.surface_get_arrays(0) + var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX] + var uv := PackedVector2Array() + for vertex in vertices: + uv.append(office_uv(vertex + c, c, m.get_meta("office_surface"))) + arrays[Mesh.ARRAY_TEX_UV] = uv + var mesh := ArrayMesh.new() + mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) + mesh.surface_set_material(0, m) + mi.mesh = mesh + mi.add_to_group("office_surfaces") + else: + mi.mesh = bm parent.add_child(mi) var cs := CollisionShape3D.new() var bs := BoxShape3D.new() @@ -181,16 +198,39 @@ func _add_box_to(parent: Node3D, s: Vector3, c: Vector3, m: StandardMaterial3D) func _surface_material(texture_name: String, rough: float) -> StandardMaterial3D: var material := StandardMaterial3D.new() - material.albedo_texture = load("res://assets/textures/" + texture_name + ".png") + var office := texture_name in ["wall", "ceiling"] + var directory := "res://assets/textures/office/" if office else "res://assets/textures/" + material.albedo_texture = load(directory + texture_name + ".png") material.albedo_color = Color.WHITE material.roughness = rough material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC material.texture_repeat = true - material.uv1_triplanar = true - material.uv1_world_triplanar = true - material.uv1_scale = Vector3.ONE * 0.5 + if office: + material.set_meta("office_surface", texture_name) + else: + # Existing floor projection intentionally unchanged. + material.uv1_triplanar = true + material.uv1_world_triplanar = true + material.uv1_scale = Vector3.ONE * 0.5 return material +## Shared room-space UVs, independent of each window/wall segment size. +static func office_uv(point: Vector3, center: Vector3, surface: String) -> Vector2: + if surface == "ceiling": + return Vector2(point.x + W * 0.5, point.z + D * 0.5) / CEILING_REPEAT_M + var along: float + if center.z < -D * 0.5: + along = point.x + W * 0.5 # north, clockwise + elif center.x > W * 0.5: + along = W + point.z + D * 0.5 # east + elif center.z > D * 0.5: + along = W + D + W * 0.5 - point.x # south + else: + along = W * 2 + D + D * 0.5 - point.z # west + # Top pixel at ceiling, bottom at floor: no vertical texture repeats. + # Half-texel inset avoids sampling the opposite edge at exact 0/1. + return Vector2(along / WALL_REPEAT_M, clampf(1.0 - point.y / H, 0.5 / 160.0, 1.0 - 0.5 / 160.0)) + func _mat(color: Color, rough: float) -> StandardMaterial3D: var m := StandardMaterial3D.new() m.albedo_color = color diff --git a/tools/extract_office_surfaces.py b/tools/extract_office_surfaces.py new file mode 100644 index 0000000..b7b7c83 --- /dev/null +++ b/tools/extract_office_surfaces.py @@ -0,0 +1,36 @@ +"""Extract original Half-Life office wall/ceiling textures without touching Steam files.""" +import argparse +import hashlib +import json +import pathlib +import struct +from PIL import Image + +parser = argparse.ArgumentParser() +parser.add_argument('wad', type=pathlib.Path, help='Original valve/halflife.wad') +args = parser.parse_args() +project = pathlib.Path(__file__).resolve().parents[1] +out = project / 'assets/textures/office' +out.mkdir(parents=True, exist_ok=True) +wad = args.wad.read_bytes() +assert wad[:4] == b'WAD3' +count, directory = struct.unpack_from(' void: + call_deferred("run_test") + +func run_test() -> void: + var world = load("res://scenes/main.tscn").instantiate() + root.add_child(world) + var surfaces = get_nodes_in_group("office_surfaces") + assert(surfaces.size() == 17) # sixteen wall segments + ceiling + for surface in surfaces: + assert(surface.mesh is ArrayMesh) + var mat = surface.mesh.surface_get_material(0) + assert(not mat.uv1_triplanar) + assert(mat.albedo_texture.resource_path.contains("/office/")) + var arrays = surface.mesh.surface_get_arrays(0) + assert(arrays[Mesh.ARRAY_TEX_UV].size() == arrays[Mesh.ARRAY_VERTEX].size()) + print("PASS: 17 superfici con UV esplicite e texture office originali") + var north := Vector3(0, 2.5, -9.15) + var low := Vector3(0, 0.75, -9.15) + var high := Vector3(0, 4.25, -9.15) + for y in [0.0, 1.5, 2.5, 3.5, 5.0]: + var p := Vector3(2, y, -9) + assert(RoomScript.office_uv(p, north, "wall").is_equal_approx(RoomScript.office_uv(p, low, "wall"))) + assert(RoomScript.office_uv(p, north, "wall").is_equal_approx(RoomScript.office_uv(p, high, "wall"))) + assert(is_equal_approx(RoomScript.office_uv(Vector3(0, 2.5, -9), north, "wall").y, 0.5)) + assert(is_equal_approx(RoomScript.office_uv(Vector3(4, 2.5, -9), north, "wall").x - RoomScript.office_uv(Vector3(0, 2.5, -9), north, "wall").x, 1)) + print("PASS: finestre senza reset UV, stessa quota verticale, periodo parete 4m") + var east := Vector3(9.15, 2.5, 0) + var south := Vector3(0, 2.5, 9.15) + var west := Vector3(-9.15, 2.5, 0) + for pair in [[north, east, Vector3(9, 2.5, -9)], [east, south, Vector3(9, 2.5, 9)], [south, west, Vector3(-9, 2.5, 9)]]: + assert(RoomScript.office_uv(pair[2], pair[0], "wall").is_equal_approx(RoomScript.office_uv(pair[2], pair[1], "wall"))) + var start := RoomScript.office_uv(Vector3(-9, 2.5, -9), north, "wall") + var end := RoomScript.office_uv(Vector3(-9, 2.5, -9), west, "wall") + assert(is_equal_approx(end.x - start.x, 18.0)) # full perimeter = integer repeats + print("PASS: fase continua ai quattro angoli, chiusura perimetro intera") + assert(RoomScript.office_uv(Vector3(-9, 5, -9), Vector3.ZERO, "ceiling") == Vector2.ZERO) + assert(RoomScript.office_uv(Vector3(9, 5, 9), Vector3.ZERO, "ceiling").is_equal_approx(Vector2(15, 15))) + assert(RoomScript.office_uv(Vector3(-7.8, 5, -7.8), Vector3.ZERO, "ceiling").is_equal_approx(Vector2.ONE)) + print("PASS: soffitto XZ, 15x15 ripetizioni, piastrelle da 60cm") + if "--screenshot" in OS.get_cmdline_user_args(): + world.player.set_physics_process(false) + world.player.position = Vector3(0, 0.1, 0) + world.player.camera.rotation.x = 0.25 + var inspection := OmniLight3D.new() + inspection.position = Vector3(0, 3, 0) + inspection.omni_range = 30 + inspection.light_energy = 1.8 + world.add_child(inspection) + for i in 3: + world.player.rotation.y = -PI * 0.25 if i == 0 else PI * 0.25 + PI + inspection.visible = i < 2 # last image retains actual horror lighting + await create_timer(0.4).timeout + await RenderingServer.frame_post_draw + root.get_texture().get_image().save_png("/tmp/zombie-office-uv-%d.png" % i) + world.queue_free() + await process_frame + quit() diff --git a/tools/test_office_uv.gd.uid b/tools/test_office_uv.gd.uid new file mode 100644 index 0000000..d36023e --- /dev/null +++ b/tools/test_office_uv.gd.uid @@ -0,0 +1 @@ +uid://8q5ib6vc01t0