Mortars, Mag Check Reload, bNVGs, fake shadows

This commit is contained in:
GetParanoid 2026-04-12 08:00:35 -07:00
parent a166d8cc4a
commit 913043475b
61 changed files with 710 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -0,0 +1,8 @@
replacement textures for NVG/Thermal overlays
copy & paste these into:
Assets/NVG/[nvg of your choice]
or
Assets/Thermal/[thermal of your choice]
be sure to rename to mask.png and replace the original file

View file

@ -0,0 +1,19 @@
{
"itemId": "5c0558060db834001b735271",
"category": "GPNVG-18",
"gain": 2.5,
"noiseIntensity": 0.2,
"noiseSize": 0.1,
"maskSize": 0.96,
"red": 152,
"green": 214,
"blue": 252,
"gatingType": "AutoGating",
"gatingSpeed": 0.3,
"minBrightness": 0.2,
"maxBrightness": 1,
"minBrightnessThreshold": 0,
"maxBrightnessThreshold": 0.15,
"edgeDistortion": 0.02,
"edgeDistortionStart": 0.04
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View file

@ -0,0 +1,19 @@
{
"itemId": "5c066e3a0db834001b7353f0",
"category": "N-15",
"gain": 2.1,
"noiseIntensity": 0.25,
"noiseSize": 0.15,
"maskSize": 1.0,
"red": 60,
"green": 235,
"blue": 100,
"gatingType": "AutoGain",
"gatingSpeed": 0.3,
"minBrightness": 0.2,
"maxBrightness": 1,
"minBrightnessThreshold": 0,
"maxBrightnessThreshold": 0.15,
"edgeDistortion": 0.1,
"edgeDistortionStart": 0.2
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -0,0 +1,19 @@
{
"itemId": "5c0696830db834001d23f5da",
"category": "PNV-10T",
"gain": 1.6,
"noiseIntensity": 0.3,
"noiseSize": 0.2,
"maskSize": 1.0,
"red": 60,
"green": 210,
"blue": 60,
"gatingType": "Off",
"gatingSpeed": 0.3,
"minBrightness": 0.2,
"maxBrightness": 1,
"minBrightnessThreshold": 0,
"maxBrightnessThreshold": 0.15,
"edgeDistortion": 0.15,
"edgeDistortionStart": 0.3
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -0,0 +1,19 @@
{
"itemId": "67506ca81f18589016006aa6",
"category": "PNV-57E",
"gain": 1.2,
"noiseIntensity": 0.35,
"noiseSize": 0.2,
"maskSize": 1.0,
"red": 80,
"green": 215,
"blue": 60,
"gatingType": "Off",
"gatingSpeed": 0.3,
"minBrightness": 0.2,
"maxBrightness": 1,
"minBrightnessThreshold": 0,
"maxBrightnessThreshold": 0.15,
"edgeDistortion": 0.55,
"edgeDistortionStart": 0.5
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -0,0 +1,19 @@
{
"itemId": "57235b6f24597759bf5a30f1",
"category": "PVS-14",
"gain": 2.4,
"noiseIntensity": 0.2,
"noiseSize": 0.1,
"maskSize": 1,
"red": 95,
"green": 210,
"blue": 255,
"gatingType": "AutoGating",
"gatingSpeed": 0.3,
"minBrightness": 0.2,
"maxBrightness": 1,
"minBrightnessThreshold": 0,
"maxBrightnessThreshold": 0.15,
"edgeDistortion": 0.02,
"edgeDistortionStart": 0.04
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View file

@ -0,0 +1,251 @@
// Written by Arys
Shader "Hidden/CustomNightVision"
{
Properties
{
// Nightvision properties
_Color ("Color", Vector) = (0.596,0.839,0.988,1)
_MainTex ("_MainTex", 2D) = "white" {}
_Intensity ("_Intensity", Float) = 2.5
_Noise ("_Noise", 2D) = "white" {}
_NoiseScale ("_NoiseScale", Vector) = (1,1.68,0,0)
_NoiseIntensity ("_NoiseIntensity", Float) = 1
_NightVisionOn ("_NightVisionOn", Float) = 1
_LensDistortionOn ("_LensDistortionOn", Float) = 1
_EdgeDistortion ("_EdgeDistortion", Float) = 0.1
_EdgeDistortionStart ("_EdgeDistortionStart", Float) = 0.28
_NearBlurOn ("_NearBlurOn", Float) = 1
_NearBlurIntensity ("_NearBlurIntensity", Float) = 20
_NearBlurMaxDistance ("_NearBlurMaxDistance", Float) = 4
_NearBlurKernel ("_NearBlurKernel", Range(1,3)) = 3
// Texture mask properties
_Mask ("_Mask", 2D) = "white" {}
_InvMaskSize ("_InvMaskSize", Float) = 1
_InvAspect ("_InvAspect", Float) = 0.42
_CameraAspect ("_CameraAspect", Float) = 1.78
}
SubShader
{
Pass
{
Cull Off
ZWrite Off
ZTest Always
Fog
{
Mode Off
}
GpuProgramID 33735
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 sv_position : SV_Position0;
float2 texcoord : TEXCOORD0;
float2 texcoord1 : TEXCOORD1;
float2 texcoord2 : TEXCOORD2;
float4 texcoord3 : TEXCOORD3;
};
struct fout
{
float4 sv_target : SV_Target0;
};
float2 _NoiseScale;
float _NoiseIntensity;
float _NightVisionOn;
float _LensDistortionOn;
float _EdgeDistortion;
float _EdgeDistortionStart;
float _NearBlurOn;
float _NearBlurIntensity;
float _NearBlurMaxDistance;
float _NearBlurKernel;
float4 _Color;
float _Intensity;
float4 _MainTex_TexelSize;
float _InvMaskSize;
float _InvAspect;
float _CameraAspect;
sampler2D _MainTex;
sampler2D _Mask;
sampler2D _Noise;
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
float ComputeNearFactor(float2 uv)
{
float rawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);
float eyeDepth = LinearEyeDepth(rawDepth);
return saturate(1.0 - eyeDepth / max(_NearBlurMaxDistance, 0.001));
}
v2f vert(appdata_full v)
{
v2f o;
float4 tmp0;
float4 tmp1;
float4 tmp2;
tmp0 = v.vertex.yyyy * unity_ObjectToWorld._m01_m11_m21_m31;
tmp0 = unity_ObjectToWorld._m00_m10_m20_m30 * v.vertex.xxxx + tmp0;
tmp0 = unity_ObjectToWorld._m02_m12_m22_m32 * v.vertex.zzzz + tmp0;
tmp0 = tmp0 + unity_ObjectToWorld._m03_m13_m23_m33;
tmp1 = tmp0.yyyy * unity_MatrixVP._m01_m11_m21_m31;
tmp1 = unity_MatrixVP._m00_m10_m20_m30 * tmp0.xxxx + tmp1;
tmp1 = unity_MatrixVP._m02_m12_m22_m32 * tmp0.zzzz + tmp1;
tmp1 = unity_MatrixVP._m03_m13_m23_m33 * tmp0.wwww + tmp1;
o.sv_position = tmp1;
if (_NightVisionOn == 0)
{
o.texcoord.xy = v.texcoord.xy;
return o;
}
tmp0.x = v.texcoord.x - 0.5;
tmp0.x *= _CameraAspect;
tmp0.z = tmp0.x * _InvAspect;
tmp0.w = v.texcoord.y;
tmp0.xy = tmp0.zw - float2(-0.0, 0.5);
tmp2.xy = _Time.xx * float2(14345.68, -12345.68);
tmp2.xy = frac(tmp2.xy);
o.texcoord1.xy = v.texcoord.xy * _NoiseScale + tmp2.xy;
o.texcoord2.xy = tmp0.xy * _InvMaskSize.xx + float2(0.5, 0.5);
o.texcoord.xy = v.texcoord.xy;
tmp0.y = tmp0.y * unity_MatrixV._m21;
tmp0.x = unity_MatrixV._m20 * tmp0.x + tmp0.y;
tmp0.x = unity_MatrixV._m22 * tmp0.z + tmp0.x;
tmp0.x = unity_MatrixV._m23 * tmp0.w + tmp0.x;
o.texcoord3.z = -tmp0.x;
tmp0.x = tmp1.y * _ProjectionParams.x;
tmp0.w = tmp0.x * 0.5;
tmp0.xz = tmp1.xw * float2(0.5, 0.5);
o.texcoord3.w = tmp1.w;
o.texcoord3.xy = tmp0.zz + tmp0.xw;
return o;
}
fout frag(v2f inp)
{
fout o;
float4 tmp0 = tex2D(_MainTex, inp.texcoord.xy);
if (_NightVisionOn == 0)
{
o.sv_target = tmp0;
return o;
}
float4 noise = tex2D(_Noise, inp.texcoord1.xy);
float4 rawMask = tex2D(_Mask, inp.texcoord2.xy);
// 1) NVG application mask (hard cutoff).
const float nvgCutoff = 0.55;
float nvgMask = step(rawMask.a, nvgCutoff);
if (nvgMask <= 0.0)
{
o.sv_target = tmp0;
return o;
}
// Prebaked mask format:
// R,G = edge direction encoded from [-1,1] to [0,1]
// B = distance-to-edge normalized [0,1] inside mask
float2 edgeDir = rawMask.rg * 2.0 - 1.0;
edgeDir.y = -edgeDir.y;
float dirLen = length(edgeDir);
if (dirLen > 1e-5)
{
edgeDir /= dirLen;
}
else
{
edgeDir = float2(0.0, 0.0);
}
float2 warpedUv = inp.texcoord.xy;
if (_LensDistortionOn > 0.5)
{
float edgeDistance = saturate(rawMask.b);
float edgeWidth = max(saturate(_EdgeDistortionStart), 0.001);
float edgeBand = 1.0 - smoothstep(0.0, edgeWidth, edgeDistance);
float edgeBias = 1.0 - edgeDistance;
// 2) Distortion strength mask (smooth falloff).
float distMask = nvgMask * edgeBand * edgeBias * edgeBias;
warpedUv = inp.texcoord.xy - edgeDir * (_EdgeDistortion * 0.1 * distMask);
warpedUv = clamp(warpedUv, 0.0, 1.0);
}
tmp0 = tex2D(_MainTex, warpedUv);
// Depth-based near Gaussian blur (strong near camera, fades to zero at max distance).
if (_NearBlurOn > 0.5)
{
float nearFactor = ComputeNearFactor(warpedUv);
float2 spreadUv = _MainTex_TexelSize.xy * max(1.0, _NearBlurIntensity * 0.35);
float nC = nearFactor;
float nR = ComputeNearFactor(clamp(warpedUv + float2( spreadUv.x, 0.0), 0.0, 1.0));
float nL = ComputeNearFactor(clamp(warpedUv + float2(-spreadUv.x, 0.0), 0.0, 1.0));
float nU = ComputeNearFactor(clamp(warpedUv + float2(0.0, spreadUv.y), 0.0, 1.0));
float nD = ComputeNearFactor(clamp(warpedUv + float2(0.0, -spreadUv.y), 0.0, 1.0));
float nUR = ComputeNearFactor(clamp(warpedUv + float2( spreadUv.x, spreadUv.y), 0.0, 1.0));
float nUL = ComputeNearFactor(clamp(warpedUv + float2(-spreadUv.x, spreadUv.y), 0.0, 1.0));
float nDR = ComputeNearFactor(clamp(warpedUv + float2( spreadUv.x, -spreadUv.y), 0.0, 1.0));
float nDL = ComputeNearFactor(clamp(warpedUv + float2(-spreadUv.x, -spreadUv.y), 0.0, 1.0));
float nearSoft = (
nUL + 2.0 * nU + nUR +
2.0 * nL + 4.0 * nC + 2.0 * nR +
nDL + 2.0 * nD + nDR
) / 16.0;
float nearPeak = max(max(max(nL, nR), max(nU, nD)), max(max(nUL, nUR), max(nDL, nDR)));
float nearFactorSpread = lerp(nearSoft, nearPeak, 0.35);
float blurRadiusPx = _NearBlurIntensity * nearFactorSpread * nvgMask;
if (blurRadiusPx > 0.001)
{
const int maxKernelRadius = 4; // 7x7 max
int kernelRadius = clamp((int)round(_NearBlurKernel), 1, maxKernelRadius);
float2 texel = _MainTex_TexelSize.xy;
float sampleScale = blurRadiusPx / kernelRadius;
float sigma = max(blurRadiusPx * 0.5, 0.75);
float invTwoSigma2 = 0.5 / (sigma * sigma);
float4 g = 0;
float wsum = 0;
[unroll]
for (int ky = -maxKernelRadius; ky <= maxKernelRadius; ky++)
{
[unroll]
for (int kx = -maxKernelRadius; kx <= maxKernelRadius; kx++)
{
if (abs(kx) > kernelRadius || abs(ky) > kernelRadius)
{
continue;
}
float2 k = float2(kx, ky);
float w = exp(-dot(k, k) * invTwoSigma2);
float2 suv = clamp(warpedUv + k * texel * sampleScale, 0.0, 1.0);
g += tex2D(_MainTex, suv) * w;
wsum += w;
}
}
float4 blurred = g / max(wsum, 1e-5);
float blurBlend = smoothstep(0.0, 0.35, nearFactorSpread);
tmp0 = lerp(tmp0, blurred, blurBlend);
}
}
noise *= _NoiseIntensity.xxxx;
noise *= nvgMask;
float4 tmp1 = tmp0;
tmp1.x += tmp1.y;
tmp1.x += tmp1.z;
tmp1 = noise + tmp1.xxxx * _Color;
tmp1 *= _Intensity.xxxx;
tmp1 = saturate(tmp1 * 0.45);
tmp0 = lerp(tmp0, tmp1, nvgMask);
o.sv_target = tmp0;
return o;
}
ENDCG
}
}
Fallback "Hidden/Internal-BlackError"
}

Binary file not shown.

View file

@ -0,0 +1,10 @@
{
"itemId": "5c110624d174af029e69734c",
"category": "T-7 Thermal",
"isFpsStuck": true,
"minFps": 60,
"maxFps": 60,
"isPixelated": true,
"isNoisy": false,
"isMotionBlurred": false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View 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())

Binary file not shown.