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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.