diff --git a/assets/textures/office/ceiling.png b/assets/textures/office/ceiling.png new file mode 100644 index 0000000..a1998cc Binary files /dev/null and b/assets/textures/office/ceiling.png differ diff --git a/assets/textures/office/ceiling.png.import b/assets/textures/office/ceiling.png.import new file mode 100644 index 0000000..24d52fe --- /dev/null +++ b/assets/textures/office/ceiling.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d3v8pgffobhn8" +path="res://.godot/imported/ceiling.png-3dc227226c222d102f30332bf6cebfa4.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/textures/office/ceiling.png" +dest_files=["res://.godot/imported/ceiling.png-3dc227226c222d102f30332bf6cebfa4.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/assets/textures/office/source_manifest.json b/assets/textures/office/source_manifest.json new file mode 100644 index 0000000..d50fd26 --- /dev/null +++ b/assets/textures/office/source_manifest.json @@ -0,0 +1,21 @@ +{ + "wad_sha256": "38ababf773463fdf6133005d13e543098fc42392b458057f239469b764691300", + "textures": { + "FIFTIES_CEIL5": { + "file": "ceiling.png", + "size": [ + 48, + 48 + ], + "lump_sha256": "b176b8d24679d302017a5b8a1dff33235150ec556d1a8759c094dbbb0b8db454" + }, + "FIFTIES_WALL14U": { + "file": "wall.png", + "size": [ + 128, + 160 + ], + "lump_sha256": "4ddf2f877bbc4bdd71e9d324fb6a5f696cdd078f85589164c378160d2a9eba36" + } + } +} diff --git a/assets/textures/office/wall.png b/assets/textures/office/wall.png new file mode 100644 index 0000000..a8b0786 Binary files /dev/null and b/assets/textures/office/wall.png differ 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