Align original Half-Life office walls and ceiling with explicit room UVs
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
+41
-1
@@ -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,6 +169,21 @@ func _add_box_to(parent: Node3D, s: Vector3, c: Vector3, m: StandardMaterial3D)
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = s
|
||||
bm.material = m
|
||||
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()
|
||||
@@ -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
|
||||
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
|
||||
|
||||
@@ -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('<ii', wad, 4)
|
||||
wanted = {'FIFTIES_WALL14U': 'wall.png', 'FIFTIES_CEIL5': 'ceiling.png'}
|
||||
manifest = {'wad_sha256': hashlib.sha256(wad).hexdigest(), 'textures': {}}
|
||||
for i in range(count):
|
||||
offset, size, _, typ, compression, _, _, raw = struct.unpack_from('<iiiBBBB16s', wad, directory + 32*i)
|
||||
name = raw.split(b'\0')[0].decode().upper()
|
||||
if name not in wanted:
|
||||
continue
|
||||
assert not compression and typ == 0x43, name
|
||||
_, w, h, pixels, _, _, last = struct.unpack_from('<16s6I', wad, offset)
|
||||
palette = offset + last + (w//8)*(h//8)
|
||||
assert struct.unpack_from('<H', wad, palette)[0] == 256
|
||||
im = Image.frombytes('P', (w, h), wad[offset+pixels:offset+pixels+w*h])
|
||||
im.putpalette(wad[palette+2:palette+2+768])
|
||||
im.convert('RGB').save(out / wanted[name])
|
||||
manifest['textures'][name] = {'file': wanted[name], 'size': [w, h],
|
||||
'lump_sha256': hashlib.sha256(wad[offset:offset+size]).hexdigest()}
|
||||
assert set(manifest['textures']) == set(wanted)
|
||||
(out / 'source_manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
||||
print(out)
|
||||
@@ -0,0 +1,60 @@
|
||||
extends SceneTree
|
||||
const RoomScript = preload("res://scripts/room.gd")
|
||||
|
||||
func _initialize() -> 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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://8q5ib6vc01t0
|
||||
Reference in New Issue
Block a user