#!/usr/bin/env python3 """Deterministic DERIVED terrain reconstruction, never a generator-return asset. The authentic opaque ordinary Source controls geometry and low-frequency colour. A bounded, coherent fine+midscale parent texture is retained as tone modulation. Material boundaries use the engine's ScaleNx4 shape, and interpolation never mixes material families. This flattens generated colour/lighting drift while retaining coherent material structure. No asset registration, frame policy, game Source or parent image is modified. Requires NumPy and Pillow; run with the bundled dependency Python. """ from __future__ import annotations import argparse import hashlib import json import math import os from pathlib import Path import stat import struct import sys import numpy as np import PIL from PIL import Image METHOD = 'source-broad-material-guidance-coherent-parent-fine-mid-logtexture-v1' DEFAULTS = dict(output_side=512, source_sigma=3.0, residual_sigma_fine=0.65, residual_sigma_coarse=10.0, residual_percentile=80.0, texture_log_gain=dict(gray=0.38, olive=0.55, soil=0.34, rim=0.06)) FAMILY_NAMES = ('gray', 'olive', 'soil', 'rim') def read_regular(path): p = Path(path).absolute() fd = os.open(p, os.O_RDONLY | os.O_NOFOLLOW) try: a = os.fstat(fd) if not stat.S_ISREG(a.st_mode): raise ValueError('input must be a regular file') with os.fdopen(fd, 'rb', closefd=False) as f: data = f.read() b = os.fstat(fd) if (a.st_dev, a.st_ino, a.st_size, a.st_mtime_ns) != (b.st_dev, b.st_ino, b.st_size, b.st_mtime_ns): raise ValueError('input changed during read') finally: os.close(fd) return data, dict(path=str(p), sha256=hashlib.sha256(data).hexdigest(), size=a.st_size, mtime_ns=a.st_mtime_ns, mode=a.st_mode) def decode_source(data): if len(data) < 20: raise ValueError('short Source PIC') header = struct.unpack_from('<5i', data) kind, count, offset, width, height = header if (kind, count, offset) != (1, 1, 0) or not (1 <= width <= 128 and 1 <= height <= 128): raise ValueError('this tool supports a single uncompressed Source PIC only') if len(data) != 20 + width * height: raise ValueError('Source payload size mismatch') indices = np.frombuffer(data[20:], dtype=np.uint8).reshape(height, width).copy() if np.any(indices >= 240): raise ValueError('only fully opaque ordinary Source indices0..239 are supported') return indices def palette_rgb(data): if len(data) != 768: raise ValueError('palette must contain256 three-channel6-bit entries') a = np.minimum(np.frombuffer(data, np.uint8).reshape(256, 3).astype(np.uint16), 63) return ((a << 2) | (a >> 4)).astype(np.uint8) def material_families(indices): """Explicit supported Raptor palette families; no per-asset painted mask.""" a = np.asarray(indices) out = np.full(a.shape, -1, dtype=np.int8) out[a < 32] = 0 out[(a >= 60) & (a <= 63)] = 1 out[((a >= 192) & (a <= 223)) | (a == 239)] = 2 out[(a >= 224) & (a <= 238)] = 3 if np.any(out < 0): raise ValueError('Source contains unsupported material family') return out def scale_nx(indices, factor): """Exact Scale2x/3x; Scale4x is two Scale2x passes, as render::upscale.""" a = np.asarray(indices) if factor == 1: return a.copy() if factor == 4: return scale_nx(scale_nx(a, 2), 2) if factor not in (2, 3) or a.ndim != 2: raise ValueError('ScaleNx factor must be1..4 on a plane') h, w = a.shape p = np.pad(a, 1, mode='edge') out = np.empty((h * factor, w * factor), dtype=a.dtype) for y in range(h): for x in range(w): nw, n, ne, west, c, east, sw, south, se = p[y:y+3, x:x+3].flatten().tolist() cells = [c] * (factor * factor) if n != south and west != east: if factor == 2: cells = [west if west == n else c, east if n == east else c, west if west == south else c, east if south == east else c] else: cells = [west if west == n else c, n if (west == n and c != ne) or (n == east and c != nw) else c, east if n == east else c, west if (west == n and c != sw) or (west == south and c != nw) else c, c, east if (n == east and c != se) or (south == east and c != ne) else c, west if west == south else c, south if (west == south and c != se) or (south == east and c != sw) else c, east if south == east else c] out[y*factor:(y+1)*factor, x*factor:(x+1)*factor] = np.array(cells).reshape(factor, factor) return out def gaussian(a, sigma): a = np.asarray(a, dtype=np.float64) if not math.isfinite(sigma) or sigma <= 0: raise ValueError('sigma must be finite and positive') radius = int(math.ceil(3 * sigma)) q = np.arange(-radius, radius + 1, dtype=np.float64) k = np.exp(-0.5 * (q / sigma) ** 2) k /= k.sum() b = a for axis in (0, 1): padding = [(0, 0)] * a.ndim padding[axis] = (radius, radius) p = np.pad(b, padding, mode='edge') b = np.zeros_like(a) for i, weight in enumerate(k): sl = [slice(None)] * a.ndim sl[axis] = slice(i, i + a.shape[axis]) b += weight * p[tuple(sl)] return b def resize_float(a, size): a = np.asarray(a, np.float32) if a.ndim == 3: return np.stack([resize_float(a[..., i], size) for i in range(a.shape[2])], -1) return np.asarray(Image.fromarray(a).resize(size, Image.Resampling.BILINEAR), np.float64) def validate_parameters(p, source_shape): if set(p) != set(DEFAULTS): raise ValueError('unknown or missing reconstruction parameter') side = p['output_side'] if type(side) is not int or not 4 * max(source_shape) <= side <= 2048 or side % (4 * max(source_shape)): raise ValueError('output must be an integer expansion of SourceNx4, at least4x') for name in ('source_sigma', 'residual_sigma_fine', 'residual_sigma_coarse'): if type(p[name]) not in (int, float) or not math.isfinite(p[name]) or not 0 < p[name] <= 16: raise ValueError('invalid smoothing parameter') if p['residual_sigma_fine'] >= p['residual_sigma_coarse']: raise ValueError('fine band must be narrower than coarse band') q = p['residual_percentile'] if type(q) not in (int, float) or not math.isfinite(q) or not 50 <= q <= 100: raise ValueError('invalid residual percentile') if set(p['texture_log_gain']) != set(FAMILY_NAMES): raise ValueError('all material residual amplitudes required') for v in p['texture_log_gain'].values(): if type(v) not in (int, float) or not math.isfinite(v) or not 0 <= v <= 0.7: raise ValueError('invalid bounded log-texture gain') def reconstruct(indices, palette, generated_rgb, parameters=None): p = json.loads(json.dumps(DEFAULTS if parameters is None else parameters)) a = np.asarray(indices) if a.ndim != 2 or a.shape[0] != a.shape[1] or a.size == 0 or np.any(a >= 240) or np.any(a < 0): raise ValueError('square opaque ordinary Source required') validate_parameters(p, a.shape) guide_indices = scale_nx(a, 4) labels = material_families(guide_indices) guide = np.asarray(palette)[guide_indices].astype(np.float64) side = p['output_side'] factor = side // labels.shape[0] owners = np.repeat(np.repeat(labels, factor, 0), factor, 1) base = np.zeros((side, side, 3), dtype=np.float64) quiet = np.ones((side, side), dtype=np.float64) guide_luma = guide @ np.array([0.2126, 0.7152, 0.0722]) for family in np.unique(labels): mask = (labels == family).astype(np.float64) weights = resize_float(gaussian(mask, p['source_sigma']), (side, side)) numerator = resize_float(gaussian(guide * mask[..., None], p['source_sigma']), (side, side)) field = numerator / np.maximum(weights[..., None], 1e-12) base[owners == family] = field[owners == family] if family == 2: second = resize_float(gaussian(guide_luma**2 * mask, p['source_sigma']), (side, side)) / np.maximum(weights, 1e-12) first = field @ np.array([0.2126, 0.7152, 0.0722]) std = np.sqrt(np.maximum(second-first**2, 0)) # Calm Source soil (e.g.20400right) receives less texture strength. # This map is derived from Source variance, never hand-painted. quiet[owners == family] = (0.35+0.65*np.clip(std/8.0,0,1))[owners == family] raw = np.asarray(generated_rgb) if raw.ndim != 3 or raw.shape[2] != 3 or raw.dtype != np.uint8: raise ValueError('generated input must be RGB uint8') parent = np.asarray(Image.fromarray(raw).resize((side, side), Image.Resampling.LANCZOS), np.float64) luma = parent @ np.array([0.2126, 0.7152, 0.0722]) band = gaussian(luma, p['residual_sigma_fine']) - gaussian(luma, p['residual_sigma_coarse']) texture = np.zeros_like(band) normalizers = {} for family in np.unique(owners): area = owners == family centered = band-band[area].mean() divisor = max(float(np.percentile(np.abs(centered[area]),p['residual_percentile'])),1.0) texture[area] = np.tanh(centered[area]/divisor) normalizers[FAMILY_NAMES[int(family)]] = divisor strengths = np.array([p['texture_log_gain'][n]for n in FAMILY_NAMES])[owners]*quiet gain = np.exp(strengths*texture) output = np.clip(np.rint(base*gain[...,None]),0,255).astype(np.uint8) base8 = np.clip(np.rint(base),0,255).astype(np.uint8) metrics = dict(method=METHOD, source_shape=list(a.shape),output_shape=list(output.shape), sourceNx4_indices_sha256=hashlib.sha256(guide_indices.astype(' parent.width or y+h > parent.height: raise ValueError('crop outside untouched parent') raw = np.array(parent.convert('RGB').crop((x, y, x+w, y+h))) derived, base, labels, metrics = reconstruct(indices, palette, raw, recipe['parameters']) prepared.append((asset, derived, base, labels, metrics, source_pin, parent_pin, crop)) out = Path(args.output).absolute() out.mkdir(exist_ok=False) tool_bytes, tool_pin = read_regular(__file__) results = [] for asset, derived, base, labels, metrics, source_pin, parent_pin, crop in prepared: image_path = out / f'{asset}-DERIVED-source-guided-material-v1.png' Image.fromarray(derived).save(image_path) Image.fromarray(base).save(out / f'{asset}-SOURCE-GUIDED-BASE-diagnostic.png') Image.fromarray(labels.astype(np.uint8)).save(out / f'{asset}-SOURCE-NX4-FAMILY-geometry-diagnostic.png') _, output_pin = read_regular(image_path) results.append(dict(asset=asset, output=output_pin, Source=source_pin, parent_raw=parent_pin, parent_crop=crop, recipe=recipe_pin, tool=tool_pin, parameters=recipe['parameters'], metrics=metrics, lineage='DERIVED scripted cleanup; generator-return parent remains untouched')) for record in [recipe_pin, palette_pin] + [p for t in prepared for p in (t[5], t[6])]: _, now = read_regular(record['path']) if now != record: raise ValueError('input changed across cleanup') runtime = dict(python=sys.version, executable=sys.executable, numpy=np.__version__, Pillow=PIL.__version__, platform=sys.platform) execution = dict(status='success', method=METHOD, recipe_sha256=recipe_pin['sha256'], tool_sha256=tool_pin['sha256'], source_sha256={r['asset']: r['Source']['sha256'] for r in results}, parent_raw_sha256=sorted({r['parent_raw']['sha256'] for r in results}), output_sha256={r['asset']: r['output']['sha256'] for r in results}, runtime=runtime) (out / 'DERIVED-LINEAGE-AND-ACTUAL-RECONSTRUCTION-METRICS.json').write_text( json.dumps(dict(status='DERIVED_OUTPUTS_NOT_MATERIAL_APPROVAL', method=METHOD, recipe=recipe_pin, tool=tool_pin, palette=palette_pin, frames=results, execution=execution, runtime=runtime), indent=2, sort_keys=True) + '\n') print(json.dumps(dict(output=str(out), frames=len(results), method=METHOD), indent=2)) if __name__ == '__main__': main()