Mortars, Mag Check Reload, bNVGs, fake shadows
This commit is contained in:
parent
a166d8cc4a
commit
913043475b
61 changed files with 710 additions and 0 deletions
303
BepInEx/plugins/BorkelRNVG/Assets/prebake_lens_masks.py
Normal file
303
BepInEx/plugins/BorkelRNVG/Assets/prebake_lens_masks.py
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
#!/usr/bin/env python3
|
||||
r"""
|
||||
Prebake lens textures for edge distortion.
|
||||
|
||||
Output encoding per pixel (RGBA):
|
||||
- R,G: direction to edge (encoded from [-1, 1] to [0, 255])
|
||||
- B: normalized edge distance in [0, 255] where:
|
||||
0 = near border
|
||||
255 = far from border (or no hit in scan range)
|
||||
- A: original alpha from source texture
|
||||
|
||||
This script reproduces the old shader-style directional scan:
|
||||
- For each inside-mask pixel, cast rays in N directions.
|
||||
- First ray hit against outside-mask gives distance sample.
|
||||
- Direction is weighted average of hit directions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Iterable, List, Sequence, Tuple
|
||||
# pip install pillow
|
||||
# python .\prebake_lens_masks.py --input-dir .\LensTextures --output-dir .\LensTextures_prebaked --num-directions 8 --max-steps 450 --threshold 0.5
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Prebake lens masks for NVG distortion.")
|
||||
parser.add_argument("--input-dir", type=Path, default=Path("LensTextures"))
|
||||
parser.add_argument("--output-dir", type=Path, default=Path("LensTextures_prebaked"))
|
||||
parser.add_argument(
|
||||
"--threshold",
|
||||
type=float,
|
||||
default=0.5,
|
||||
help="Inside-mask threshold in [0,1] after alpha processing.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-invert-alpha",
|
||||
action="store_true",
|
||||
help="Use alpha directly. Default uses (1-alpha), matching your shader.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-steps",
|
||||
type=int,
|
||||
default=450,
|
||||
help="Raymarch steps (old shader equivalent).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-directions",
|
||||
type=int,
|
||||
default=16,
|
||||
help="Direction count. Use 8 for old behavior, 16+ for higher quality.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def clamp01(v: float) -> float:
|
||||
if v < 0.0:
|
||||
return 0.0
|
||||
if v > 1.0:
|
||||
return 1.0
|
||||
return v
|
||||
|
||||
|
||||
def to_u8(v: float) -> int:
|
||||
return int(max(0, min(255, round(v))))
|
||||
|
||||
|
||||
def generate_dirs(count: int) -> List[Tuple[float, float]]:
|
||||
dirs: List[Tuple[float, float]] = []
|
||||
for i in range(count):
|
||||
a = (2.0 * math.pi * i) / count
|
||||
dirs.append((math.cos(a), math.sin(a)))
|
||||
return dirs
|
||||
|
||||
|
||||
def bilinear_sample(mask: Sequence[float], w: int, h: int, u: float, v: float) -> float:
|
||||
u = clamp01(u)
|
||||
v = clamp01(v)
|
||||
|
||||
x = u * w - 0.5
|
||||
y = v * h - 0.5
|
||||
|
||||
x0 = int(math.floor(x))
|
||||
y0 = int(math.floor(y))
|
||||
x1 = x0 + 1
|
||||
y1 = y0 + 1
|
||||
|
||||
if x0 < 0:
|
||||
x0 = 0
|
||||
if y0 < 0:
|
||||
y0 = 0
|
||||
if x1 >= w:
|
||||
x1 = w - 1
|
||||
if y1 >= h:
|
||||
y1 = h - 1
|
||||
|
||||
fx = x - x0
|
||||
fy = y - y0
|
||||
|
||||
i00 = y0 * w + x0
|
||||
i10 = y0 * w + x1
|
||||
i01 = y1 * w + x0
|
||||
i11 = y1 * w + x1
|
||||
|
||||
s00 = mask[i00]
|
||||
s10 = mask[i10]
|
||||
s01 = mask[i01]
|
||||
s11 = mask[i11]
|
||||
|
||||
sx0 = s00 + (s10 - s00) * fx
|
||||
sx1 = s01 + (s11 - s01) * fx
|
||||
return sx0 + (sx1 - sx0) * fy
|
||||
|
||||
|
||||
def prebake_one(
|
||||
src_path: Path,
|
||||
out_dir: Path,
|
||||
threshold: float,
|
||||
invert_alpha: bool,
|
||||
max_steps: int,
|
||||
dirs: Sequence[Tuple[float, float]],
|
||||
) -> None:
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError as exc:
|
||||
raise RuntimeError("Missing dependency: Pillow. Install with: pip install pillow") from exc
|
||||
|
||||
img = Image.open(src_path).convert("RGBA")
|
||||
w, h = img.size
|
||||
src = list(img.getdata())
|
||||
|
||||
alpha: List[int] = [px[3] for px in src]
|
||||
mask: List[float] = []
|
||||
inside: List[bool] = []
|
||||
|
||||
for a_u8 in alpha:
|
||||
a = a_u8 / 255.0
|
||||
m = (1.0 - a) if invert_alpha else a
|
||||
mask.append(m)
|
||||
inside.append(m > threshold)
|
||||
|
||||
du = 1.0 / w
|
||||
dv = 1.0 / h
|
||||
dir_count = len(dirs)
|
||||
n = w * h
|
||||
out: List[Tuple[int, int, int, int]] = [(0, 0, 0, 0)] * n
|
||||
|
||||
inside_count = 0
|
||||
for y in range(h):
|
||||
base = y * w
|
||||
v0 = (y + 0.5) * dv
|
||||
|
||||
for x in range(w):
|
||||
i = base + x
|
||||
a_u8 = alpha[i]
|
||||
|
||||
if not inside[i]:
|
||||
out[i] = (0, 0, 0, a_u8)
|
||||
continue
|
||||
|
||||
inside_count += 1
|
||||
u0 = (x + 0.5) * du
|
||||
|
||||
nearest_norm = 1.0
|
||||
accum_x = 0.0
|
||||
accum_y = 0.0
|
||||
accum_w = 0.0
|
||||
|
||||
for dx, dy in dirs:
|
||||
hit_norm = 1.0
|
||||
ray_u = u0
|
||||
ray_v = v0
|
||||
step_u = dx * du
|
||||
step_v = dy * dv
|
||||
|
||||
hit = False
|
||||
for step in range(1, max_steps + 1):
|
||||
ray_u += step_u
|
||||
ray_v += step_v
|
||||
m = bilinear_sample(mask, w, h, ray_u, ray_v)
|
||||
if m <= threshold:
|
||||
hit_norm = step / float(max_steps)
|
||||
hit = True
|
||||
break
|
||||
|
||||
if not hit:
|
||||
continue
|
||||
|
||||
if hit_norm < nearest_norm:
|
||||
nearest_norm = hit_norm
|
||||
|
||||
wdir = 1.0 - hit_norm
|
||||
wdir *= wdir
|
||||
accum_x += dx * wdir
|
||||
accum_y += dy * wdir
|
||||
accum_w += wdir
|
||||
|
||||
if accum_w > 1e-8:
|
||||
inv = 1.0 / accum_w
|
||||
ex = accum_x * inv
|
||||
ey = accum_y * inv
|
||||
el = math.hypot(ex, ey)
|
||||
if el > 1e-8:
|
||||
ex /= el
|
||||
ey /= el
|
||||
else:
|
||||
ex = 0.0
|
||||
ey = 0.0
|
||||
else:
|
||||
ex = 0.0
|
||||
ey = 0.0
|
||||
|
||||
r = to_u8((ex * 0.5 + 0.5) * 255.0)
|
||||
g = to_u8((ey * 0.5 + 0.5) * 255.0)
|
||||
b = to_u8(clamp01(nearest_norm) * 255.0)
|
||||
out[i] = (r, g, b, a_u8)
|
||||
|
||||
print(f"[{src_path.name}] row {y + 1}/{h}", end="\r")
|
||||
|
||||
print(f"[{src_path.name}] rows done: {h}/{h} ")
|
||||
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
out_path = out_dir / f"{src_path.stem}.png"
|
||||
out_img = Image.new("RGBA", (w, h))
|
||||
out_img.putdata(out)
|
||||
out_img.save(out_path)
|
||||
"""
|
||||
meta = {
|
||||
"source": str(src_path),
|
||||
"output": str(out_path),
|
||||
"size": [w, h],
|
||||
"inside_threshold": threshold,
|
||||
"invert_alpha_used": invert_alpha,
|
||||
"max_steps": max_steps,
|
||||
"direction_count": dir_count,
|
||||
"inside_pixels": inside_count,
|
||||
"encoding": {
|
||||
"R": "edge direction X encoded from [-1,1] -> [0,255]",
|
||||
"G": "edge direction Y encoded from [-1,1] -> [0,255]",
|
||||
"B": "nearest edge distance normalized [0,1] by max_steps",
|
||||
"A": "original alpha",
|
||||
},
|
||||
}
|
||||
(out_dir / f"{src_path.stem}_prebaked.json").write_text(
|
||||
json.dumps(meta, indent=2), encoding="utf-8"
|
||||
)
|
||||
"""
|
||||
print(f"[OK] {src_path.name} -> {out_path.name}")
|
||||
|
||||
|
||||
def iter_pngs(folder: Path) -> Iterable[Path]:
|
||||
for p in sorted(folder.glob("*.png")):
|
||||
if p.is_file():
|
||||
yield p
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
input_dir: Path = args.input_dir
|
||||
output_dir: Path = args.output_dir
|
||||
threshold: float = float(args.threshold)
|
||||
invert_alpha: bool = not args.no_invert_alpha
|
||||
max_steps: int = int(args.max_steps)
|
||||
num_directions: int = int(args.num_directions)
|
||||
|
||||
if not input_dir.exists():
|
||||
raise FileNotFoundError(f"Input dir not found: {input_dir}")
|
||||
if not (0.0 <= threshold <= 1.0):
|
||||
raise ValueError("--threshold must be in [0,1]")
|
||||
if max_steps <= 0:
|
||||
raise ValueError("--max-steps must be > 0")
|
||||
if num_directions < 4:
|
||||
raise ValueError("--num-directions must be >= 4")
|
||||
|
||||
dirs = generate_dirs(num_directions)
|
||||
pngs = list(iter_pngs(input_dir))
|
||||
if not pngs:
|
||||
print(f"No PNG files found in: {input_dir}")
|
||||
return 0
|
||||
|
||||
print(
|
||||
f"Prebake config: dirs={len(dirs)}, max_steps={max_steps}, "
|
||||
f"threshold={threshold}, invert_alpha={invert_alpha}"
|
||||
)
|
||||
|
||||
for src in pngs:
|
||||
prebake_one(
|
||||
src_path=src,
|
||||
out_dir=output_dir,
|
||||
threshold=threshold,
|
||||
invert_alpha=invert_alpha,
|
||||
max_steps=max_steps,
|
||||
dirs=dirs,
|
||||
)
|
||||
|
||||
print(f"Done. Wrote {len(pngs)} textures to: {output_dir}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue