summaryrefslogtreecommitdiffstats
path: root/LUTS/ssfd_lut
diff options
context:
space:
mode:
authoryum <yum.food.vr@gmail.com>2026-03-31 21:52:22 -0700
committeryum <yum.food.vr@gmail.com>2026-03-31 21:52:22 -0700
commit072fa8904087ad780ef09132e0e0717d0eecdb68 (patch)
tree9106d1a919f1e54c6895b976053b6d732651ce48 /LUTS/ssfd_lut
parent57aa53c6b1b51265839dbd71aac4eeb88e050de0 (diff)
ssfd bugfixes
Diffstat (limited to 'LUTS/ssfd_lut')
-rwxr-xr-xLUTS/ssfd_lut165
1 files changed, 165 insertions, 0 deletions
diff --git a/LUTS/ssfd_lut b/LUTS/ssfd_lut
new file mode 100755
index 0000000..38391b0
--- /dev/null
+++ b/LUTS/ssfd_lut
@@ -0,0 +1,165 @@
+#!/usr/bin/env -S uv run --script
+# /// script
+# requires-python = ">=3.10"
+# dependencies = []
+# ///
+
+from __future__ import annotations
+
+import math
+import struct
+import sys
+import zlib
+from pathlib import Path
+
+
+PIXELS_PER_BAYER_CELL = 16
+TOTAL_DOT_AREA = 0.5
+SDF_RADIUS_SCALE = 2.4
+BINARY_THRESHOLD = 2.0 / 3.0
+BASE_BAYER_POINTS = (
+ (0.0, 0.0),
+ (0.5, 0.5),
+ (0.5, 0.0),
+ (0.0, 0.5),
+)
+
+
+def main(argv: list[str]) -> int:
+ if len(argv) != 2:
+ print(f"usage: {Path(argv[0]).name} N", file=sys.stderr)
+ return 2
+
+ try:
+ order = int(argv[1])
+ except ValueError:
+ print("N must be an integer.", file=sys.stderr)
+ return 2
+
+ if order < 1 or order & (order - 1):
+ print("N must be a positive power of two.", file=sys.stderr)
+ return 2
+
+ output_dir = Path.cwd() / f"SSFD_{order}x{order}"
+ output_dir.mkdir(parents=True, exist_ok=True)
+ clear_previous_outputs(output_dir)
+
+ size = order * PIXELS_PER_BAYER_CELL
+ points = build_bayer_points(order)
+ min_dist_sq = [math.inf] * (size * size)
+ sample_x = [((x + 0.5) / size) for x in range(size)]
+ sample_y = [((y + 0.5) / size) for y in range(size)]
+
+ for layer_index, point in enumerate(points, start=1):
+ update_min_distances(min_dist_sq, sample_x, sample_y, size, point)
+ write_layer(output_dir, min_dist_sq, size, layer_index)
+
+ print(f"Wrote {len(points)} textures to {output_dir}")
+ return 0
+
+
+def clear_previous_outputs(output_dir: Path) -> None:
+ for pattern in (
+ "dots_L*.png",
+ "dots_L*-sdf.png",
+ "dots_L*.png.meta",
+ "dots_L*-sdf.png.meta",
+ ):
+ for path in output_dir.glob(pattern):
+ if path.is_file():
+ path.unlink()
+
+
+def build_bayer_points(order: int) -> list[tuple[float, float]]:
+ if order == 1:
+ return [(0.0, 0.0)]
+
+ recursion = int(math.log2(order))
+ points = list(BASE_BAYER_POINTS)
+ for r in range(recursion - 1):
+ count = len(points)
+ offset = 0.5 ** (r + 1)
+ for i in range(1, 4):
+ dx, dy = BASE_BAYER_POINTS[i]
+ for j in range(count):
+ px, py = points[j]
+ points.append((px + dx * offset, py + dy * offset))
+ return points
+
+
+def update_min_distances(
+ min_dist_sq: list[float],
+ sample_x: list[float],
+ sample_y: list[float],
+ size: int,
+ point: tuple[float, float],
+) -> None:
+ px, py = point
+ for y, sy in enumerate(sample_y):
+ dy = wrap_delta(sy - py)
+ dy_sq = dy * dy
+ row_offset = y * size
+ for x, sx in enumerate(sample_x):
+ dx = wrap_delta(sx - px)
+ dist_sq = dx * dx + dy_sq
+ idx = row_offset + x
+ if dist_sq < min_dist_sq[idx]:
+ min_dist_sq[idx] = dist_sq
+
+
+def write_layer(output_dir: Path, min_dist_sq: list[float], size: int, layer_index: int) -> None:
+ dot_radius = math.sqrt((TOTAL_DOT_AREA / layer_index) / math.pi)
+ scale = dot_radius * SDF_RADIUS_SCALE
+ sdf_pixels = bytearray()
+ binary_pixels = bytearray()
+
+ for dist_sq in min_dist_sq:
+ dist = math.sqrt(dist_sq)
+ value = clamp01(1.0 - dist / scale)
+ sdf_byte = round(value * 255.0)
+ sdf_pixels.append(sdf_byte)
+ binary_pixels.append(255 if value >= BINARY_THRESHOLD else 0)
+
+ write_grayscale_png(output_dir / f"dots_L{layer_index}-sdf.png", size, size, sdf_pixels)
+ write_grayscale_png(output_dir / f"dots_L{layer_index}.png", size, size, binary_pixels)
+
+
+def wrap_delta(value: float) -> float:
+ return (value + 0.5) % 1.0 - 0.5
+
+
+def clamp01(value: float) -> float:
+ if value <= 0.0:
+ return 0.0
+ if value >= 1.0:
+ return 1.0
+ return value
+
+
+def write_grayscale_png(path: Path, width: int, height: int, pixels: bytearray) -> None:
+ raw = bytearray()
+ stride = width
+ for y in range(height):
+ start = y * stride
+ raw.append(0)
+ raw.extend(pixels[start : start + stride])
+
+ compressed = zlib.compress(bytes(raw), level=9)
+ with path.open("wb") as handle:
+ handle.write(b"\x89PNG\r\n\x1a\n")
+ handle.write(make_chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 0, 0, 0, 0)))
+ handle.write(make_chunk(b"IDAT", compressed))
+ handle.write(make_chunk(b"IEND", b""))
+
+
+def make_chunk(chunk_type: bytes, payload: bytes) -> bytes:
+ return (
+ struct.pack(">I", len(payload))
+ + chunk_type
+ + payload
+ + struct.pack(">I", zlib.crc32(chunk_type + payload) & 0xFFFFFFFF)
+ )
+
+
+if __name__ == "__main__":
+ raise SystemExit(main(sys.argv))