local-extract: derive the board normal map from both layouts a client holds — BC5 (R = X) and DXT5nm (A = X, R = 1), read as Unity unpacks them (x = R·A); a client with the older map_autochess bundle got Z = 0 everywhere and a grey 3D board
This commit is contained in:
3 files changed
+36
-15
No files matched your search
@@ -1,8 +1,8 @@
|
||||
// render/board3d/materials.js — three.js materials of the official board scene (DESIGN §15). THREE is injected
|
||||
// (the renderer loads /vendor/three.module.js; Node tests pass the npm package).
|
||||
//
|
||||
// MT_autochess → MeshStandardMaterial: albedo TX_autochessi_D (sRGB), normal TX_autochessi_N (BC5 → RGB,
|
||||
// tools/local-extract DERIVED), roughness 1 − smoothness of TX_autochessi_M (DERIVED), metal 0,
|
||||
// MT_autochess → MeshStandardMaterial: albedo TX_autochessi_D (sRGB), normal TX_autochessi_N (BC5 / DXT5nm →
|
||||
// RGB, tools/local-extract DERIVED), roughness 1 − smoothness of TX_autochessi_M (DERIVED), metal 0,
|
||||
// emission TX_autochessi_E (_EmissionColor white), vertex colours = baked AO / tints
|
||||
// MT_autochess_common → device plates of TX_autochessi_common_D, alpha test 0.408, emission _E × 0.559
|
||||
// MT_autochessi_BG → unlit TX_autochessi_BG × 0.713 (Torappu/Unlit/Texture, _Color)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// test/render/board3d-extract.test.js — the local-client exports behind the official 3D board (DESIGN §13/§15):
|
||||
// tools/local-extract/extract.py's board jobs (gate / objective effects, blower texture, water & noise maps), the
|
||||
// derived three.js maps (BC5 normal → RGB normal, Unity metallic/gloss → roughness), the WebP copies of the textures
|
||||
// derived three.js maps (BC5 / DXT5nm normal → RGB normal, Unity metallic/gloss → roughness), the WebP copies of the textures
|
||||
// the renderer downloads (extract.py WEBP), the prefab mesh keys, and —
|
||||
// when a client was extracted here — the files and manifest entries the renderer loads (render/board3d/load.js).
|
||||
|
||||
@@ -65,6 +65,23 @@ print(json.dumps({'n': [list(rn.getpixel((i, 0))) for i in range(3)], 'm': [list
|
||||
assert.deepEqual(out.m, [[255, 255, 0], [255, 55, 0]]);
|
||||
});
|
||||
|
||||
test('derived normal map: the DXT5nm layout (R = 1, G = Y, A = X) gives the same normals as BC5 (R = X, G = Y)', { skip: !HAS_PIL && 'no Pillow' }, () => {
|
||||
const out = py(`
|
||||
from PIL import Image
|
||||
xy = [(128, 128), (255, 128), (128, 20), (90, 200), (0, 128), (37, 37)]
|
||||
bc5 = Image.new('RGB', (len(xy), 1)); nm = Image.new('RGBA', (len(xy), 1)); opaque = Image.new('RGBA', (len(xy), 1))
|
||||
for i, (x, y) in enumerate(xy):
|
||||
bc5.putpixel((i, 0), (x, y, 0)); nm.putpixel((i, 0), (255, y, y, x)); opaque.putpixel((i, 0), (x, y, 0, 255))
|
||||
px = lambda im: [list(im.getpixel((i, 0))) for i in range(len(xy))]
|
||||
print(json.dumps({'bc5': px(e.derive_normal_rg(bc5)), 'nm': px(e.derive_normal_rg(nm)), 'opaque': px(e.derive_normal_rg(opaque)),
|
||||
'mode': e.derive_normal_rg(nm).mode, 'xy': xy}))`);
|
||||
assert.equal(out.mode, 'RGB');
|
||||
assert.deepEqual(out.nm, out.bc5, 'X read from A when R is 1');
|
||||
assert.deepEqual(out.opaque, out.bc5, 'a BC5 map decoded with an opaque alpha');
|
||||
assert.deepEqual(out.nm.map((p) => p.slice(0, 2)), out.xy, 'XY kept');
|
||||
assert.ok(out.nm[0][2] >= 254 && out.nm[3][2] > 128, 'z rebuilt, not the 128 of x = 1');
|
||||
});
|
||||
|
||||
test('WebP copies: exactly the board textures the renderer downloads; normal and data maps stay lossless', () => {
|
||||
const r = spawnSync(PY, [path.join(TOOL, 'extract.py'), '--print-jobs'], { encoding: 'utf8', env: ENV });
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
@@ -182,7 +199,7 @@ describe('extracted board files (when a local client was extracted)', { skip: !m
|
||||
const n = decodePng(readFileSync(onDisk(pngOf(g.TX_autochessi_N_rgb.path))));
|
||||
let zSum = 0, cnt = 0;
|
||||
for (let i = 0; i < n.rgba.length; i += 4 * 97) { zSum += n.rgba[i + 2]; cnt++; }
|
||||
assert.ok(zSum / cnt > 230, `mean z ${zSum / cnt}`);
|
||||
assert.ok(zSum / cnt > 230, `mean z ${zSum / cnt} (128 = made by an extract.py that read a DXT5nm map as BC5: re-extract with --only map/autochess)`);
|
||||
const m = decodePng(readFileSync(onDisk(g.TX_autochessi_M.path)));
|
||||
const r = decodePng(readFileSync(onDisk(pngOf(g.TX_autochessi_M_rough.path))));
|
||||
assert.deepEqual([r.w, r.h], [m.w, m.h]);
|
||||
|
||||
@@ -28,9 +28,10 @@ aklz4.py registers a decoder for it. This script pulls the art the web sources l
|
||||
[opt]end_box / [opt]start_fly meshes, their merged additive textures, materials and prefab), the blower's texture
|
||||
(arts/maps/common/res.ab → map/common: TX_wind_device of the s_wind_device mesh) and the shared water / noise
|
||||
textures (arts/maps/effect.ab → map/water) used for deep-sea / mire / smog tiles
|
||||
- derived PBR maps for three.js (DERIVED): Unity stores the theme's normal map as two channels (BC5: RG = XY, B = 0)
|
||||
and metallic/gloss with the smoothness in A; the board renderer needs an RGB normal map (Z rebuilt) and a
|
||||
roughness map in G (1 − smoothness; metalness B = 0), written next to the source as <name>_rgb.png / _rough.png
|
||||
- derived PBR maps for three.js (DERIVED): Unity stores the theme's normal map as two channels (BC5: RG = XY, B = 0;
|
||||
DXT5nm in an older bundle: AG = XY, R = 1) and metallic/gloss with the smoothness in A; the board renderer needs
|
||||
an RGB normal map (Z rebuilt) and a roughness map in G (1 − smoothness; metalness B = 0), written next to the
|
||||
source as <name>_rgb.png / _rough.png
|
||||
- WebP copies of the board textures every player downloads with the 3D board (WEBP), written next to their PNGs;
|
||||
the manifest lists the copy
|
||||
|
||||
@@ -148,8 +149,8 @@ TOKEN_PACKS = 'pkgrps/btl_pfb_tokens_*.ab'
|
||||
TOKEN_SPINE_SUB = 'spine/token'
|
||||
|
||||
# Derived three.js maps: (output subdir, source texture, kind, output name). 'normal_rg' rebuilds Z of a two-channel
|
||||
# (BC5) normal map into an RGB tangent-space map; 'rough_from_gloss' turns Unity's metallic/gloss map (smoothness in
|
||||
# A) into a roughness (G) / metalness (B) map. Written after the job that exported the source texture.
|
||||
# (BC5 or DXT5nm) normal map into an RGB tangent-space map; 'rough_from_gloss' turns Unity's metallic/gloss map
|
||||
# (smoothness in A) into a roughness (G) / metalness (B) map. Written after the job that exported the source texture.
|
||||
DERIVED = [
|
||||
('map/autochess', 'TX_autochessi_N', 'normal_rg', 'TX_autochessi_N_rgb'),
|
||||
('map/autochess', 'TX_autochessi_M', 'rough_from_gloss', 'TX_autochessi_M_rough'),
|
||||
@@ -400,13 +401,16 @@ def _normal_z_table():
|
||||
|
||||
|
||||
def derive_normal_rg(img):
|
||||
"""RGB tangent-space normal map from a two-channel (BC5: R = X, G = Y, B unused) one: Z rebuilt per pixel."""
|
||||
from PIL import Image
|
||||
rgb = img.convert('RGB')
|
||||
r, g, _ = rgb.split()
|
||||
"""RGB tangent-space normal map from Unity's two-channel one: Z rebuilt per pixel. X and Y are read the way Unity's
|
||||
shaders unpack them (UnityCG.cginc UnpackNormalmapRGorAG: x = R · A, y = G), which covers both layouts a client
|
||||
build holds: BC5 (R = X, G = Y, B unused, no alpha → A = 1) and DXT5nm (R = 1, G = Y, A = X — the map_autochess
|
||||
bundle before the client update of 2026-10)."""
|
||||
from PIL import Image, ImageChops
|
||||
r, g, _, a = img.convert('RGBA').split()
|
||||
x = ImageChops.multiply(r, a) # exact when either factor is 255
|
||||
table = _normal_z_table()
|
||||
z = bytes(table[(x << 8) | y] for x, y in zip(r.tobytes(), g.tobytes()))
|
||||
return Image.merge('RGB', (r, g, Image.frombytes('L', rgb.size, z)))
|
||||
z = bytes(table[(xb << 8) | yb] for xb, yb in zip(x.tobytes(), g.tobytes()))
|
||||
return Image.merge('RGB', (x, g, Image.frombytes('L', img.size, z)))
|
||||
|
||||
|
||||
def derive_rough_from_gloss(img):
|
||||
|
||||
Reference in new issue
Block a user