diff options
| author | yum <yum.food.vr@gmail.com> | 2026-03-31 21:52:22 -0700 |
|---|---|---|
| committer | yum <yum.food.vr@gmail.com> | 2026-03-31 21:52:22 -0700 |
| commit | 072fa8904087ad780ef09132e0e0717d0eecdb68 (patch) | |
| tree | 9106d1a919f1e54c6895b976053b6d732651ce48 /LUTS/ssfd_lut | |
| parent | 57aa53c6b1b51265839dbd71aac4eeb88e050de0 (diff) | |
ssfd bugfixes
Diffstat (limited to 'LUTS/ssfd_lut')
| -rwxr-xr-x | LUTS/ssfd_lut | 165 |
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)) |
