render iteration
This commit is contained in:
62
textures/scripts/README.md
Normal file
62
textures/scripts/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Texture Generation Scripts
|
||||
|
||||
## Blue Noise Generator
|
||||
|
||||
`generate_blue_noise.py` - Generates blue noise textures for high-quality dithering effects.
|
||||
|
||||
### Requirements
|
||||
|
||||
```bash
|
||||
pip install numpy pillow scipy
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
Basic usage (generates 128x128 texture):
|
||||
```bash
|
||||
python generate_blue_noise.py
|
||||
```
|
||||
|
||||
Custom size:
|
||||
```bash
|
||||
python generate_blue_noise.py --width 256 --height 256
|
||||
```
|
||||
|
||||
Custom output path:
|
||||
```bash
|
||||
python generate_blue_noise.py --output ../my_blue_noise.png
|
||||
```
|
||||
|
||||
Advanced options:
|
||||
```bash
|
||||
python generate_blue_noise.py --width 128 --height 128 --sigma 1.5 --method void_cluster
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- `--width`: Texture width in pixels (default: 128)
|
||||
- `--height`: Texture height in pixels (default: 128)
|
||||
- `--method`: Generation method
|
||||
- `void_cluster`: High-quality void-and-cluster method (default, recommended)
|
||||
- `annealing`: Simulated annealing method (slower)
|
||||
- `--sigma`: Gaussian kernel sigma for void_cluster method (default: 1.5)
|
||||
- Lower values (0.8-1.2): Tighter clustering, more high-frequency
|
||||
- Higher values (2.0-3.0): Smoother distribution
|
||||
- `--iterations`: Number of iterations (optional, auto-calculated if not specified)
|
||||
- `--output`: Output file path (default: ../blue_noise.png)
|
||||
|
||||
### What is Blue Noise?
|
||||
|
||||
Blue noise is a type of noise with energy concentrated in high frequencies and minimal low-frequency content. This makes it ideal for dithering because:
|
||||
|
||||
- No visible patterns or clustering
|
||||
- Smooth gradients without banding
|
||||
- Perceptually pleasing distribution
|
||||
- Better than Bayer or white noise for transparency effects
|
||||
|
||||
### Use Cases in snow_trail_sdl
|
||||
|
||||
- **Tree dissolve effect**: Dither trees between camera and player for unobstructed view
|
||||
- **Temporal effects**: Screen-space dithering for transitions
|
||||
- **Transparency**: High-quality alpha dithering
|
||||
- **LOD transitions**: Smooth fade between detail levels
|
||||
155
textures/scripts/generate_blue_noise.py
Executable file
155
textures/scripts/generate_blue_noise.py
Executable file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
def gaussian_kernel(size, sigma):
|
||||
x = np.arange(-size // 2 + 1, size // 2 + 1)
|
||||
y = np.arange(-size // 2 + 1, size // 2 + 1)
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
kernel = np.exp(-(xx**2 + yy**2) / (2 * sigma**2))
|
||||
return kernel / kernel.sum()
|
||||
|
||||
def apply_periodic_filter(binary_pattern, kernel):
|
||||
from scipy.signal import fftconvolve
|
||||
return fftconvolve(binary_pattern, kernel, mode='same')
|
||||
|
||||
def generate_blue_noise_void_cluster(width, height, sigma=1.5, iterations=None):
|
||||
if iterations is None:
|
||||
iterations = width * height
|
||||
|
||||
size = width * height
|
||||
pattern = np.zeros((height, width), dtype=np.float32)
|
||||
|
||||
kernel_size = int(6 * sigma)
|
||||
if kernel_size % 2 == 0:
|
||||
kernel_size += 1
|
||||
kernel = gaussian_kernel(kernel_size, sigma)
|
||||
|
||||
initial_pattern = np.random.rand(height, width)
|
||||
|
||||
print(f"Generating {width}x{height} blue noise texture...")
|
||||
print(f"Kernel size: {kernel_size}x{kernel_size}, sigma: {sigma}")
|
||||
|
||||
dither_array = np.zeros(size, dtype=np.int32)
|
||||
binary_pattern = np.zeros((height, width), dtype=np.float32)
|
||||
|
||||
for i in range(size):
|
||||
if i % (size // 10) == 0:
|
||||
print(f"Progress: {i}/{size} ({100*i//size}%)")
|
||||
|
||||
filtered = apply_periodic_filter(binary_pattern, kernel)
|
||||
|
||||
if i < size // 2:
|
||||
initial_energy = initial_pattern + filtered
|
||||
coords = np.unravel_index(np.argmax(initial_energy), initial_energy.shape)
|
||||
else:
|
||||
coords = np.unravel_index(np.argmin(filtered), filtered.shape)
|
||||
|
||||
dither_array[i] = coords[0] * width + coords[1]
|
||||
binary_pattern[coords[0], coords[1]] = 1.0
|
||||
|
||||
print("Converting to threshold map...")
|
||||
|
||||
threshold_map = np.zeros((height, width), dtype=np.float32)
|
||||
for rank, pos in enumerate(dither_array):
|
||||
y = pos // width
|
||||
x = pos % width
|
||||
threshold_map[y, x] = rank / size
|
||||
|
||||
print("Done!")
|
||||
return threshold_map
|
||||
|
||||
def generate_blue_noise_simulated_annealing(width, height, iterations=10000):
|
||||
print(f"Generating {width}x{height} blue noise using simulated annealing...")
|
||||
|
||||
pattern = np.random.rand(height, width)
|
||||
|
||||
def energy(pattern):
|
||||
fft = np.fft.fft2(pattern)
|
||||
power = np.abs(fft) ** 2
|
||||
|
||||
h, w = pattern.shape
|
||||
cy, cx = h // 2, w // 2
|
||||
y, x = np.ogrid[:h, :w]
|
||||
dist = np.sqrt((x - cx)**2 + (y - cy)**2)
|
||||
|
||||
low_freq_mask = dist < min(h, w) * 0.1
|
||||
low_freq_energy = np.sum(power * low_freq_mask)
|
||||
|
||||
return low_freq_energy
|
||||
|
||||
current_energy = energy(pattern)
|
||||
temperature = 1.0
|
||||
cooling_rate = 0.9995
|
||||
|
||||
for i in range(iterations):
|
||||
if i % (iterations // 10) == 0:
|
||||
print(f"Iteration {i}/{iterations}, Energy: {current_energy:.2f}, Temp: {temperature:.4f}")
|
||||
|
||||
y1, x1 = np.random.randint(0, height), np.random.randint(0, width)
|
||||
y2, x2 = np.random.randint(0, height), np.random.randint(0, width)
|
||||
|
||||
pattern[y1, x1], pattern[y2, x2] = pattern[y2, x2], pattern[y1, x1]
|
||||
|
||||
new_energy = energy(pattern)
|
||||
delta_energy = new_energy - current_energy
|
||||
|
||||
if delta_energy < 0 or np.random.rand() < np.exp(-delta_energy / temperature):
|
||||
current_energy = new_energy
|
||||
else:
|
||||
pattern[y1, x1], pattern[y2, x2] = pattern[y2, x2], pattern[y1, x1]
|
||||
|
||||
temperature *= cooling_rate
|
||||
|
||||
print("Done!")
|
||||
return pattern
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Generate blue noise texture for dithering')
|
||||
parser.add_argument('--width', type=int, default=128, help='Texture width (default: 128)')
|
||||
parser.add_argument('--height', type=int, default=128, help='Texture height (default: 128)')
|
||||
parser.add_argument('--method', choices=['void_cluster', 'annealing'], default='void_cluster',
|
||||
help='Generation method (default: void_cluster)')
|
||||
parser.add_argument('--sigma', type=float, default=1.5,
|
||||
help='Gaussian kernel sigma for void_cluster method (default: 1.5)')
|
||||
parser.add_argument('--iterations', type=int, default=None,
|
||||
help='Number of iterations (optional)')
|
||||
parser.add_argument('--output', type=str, default='../blue_noise.png',
|
||||
help='Output file path (default: ../blue_noise.png)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
from scipy.signal import fftconvolve
|
||||
except ImportError:
|
||||
print("Error: scipy is required for this script.")
|
||||
print("Install it with: pip install scipy")
|
||||
return
|
||||
|
||||
if args.method == 'void_cluster':
|
||||
noise = generate_blue_noise_void_cluster(args.width, args.height, args.sigma, args.iterations)
|
||||
else:
|
||||
noise = generate_blue_noise_simulated_annealing(args.width, args.height,
|
||||
args.iterations or 10000)
|
||||
|
||||
noise_normalized = ((noise - noise.min()) / (noise.max() - noise.min()) * 255).astype(np.uint8)
|
||||
|
||||
img = Image.fromarray(noise_normalized, mode='L')
|
||||
|
||||
output_path = Path(__file__).parent / args.output
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
img.save(output_path)
|
||||
|
||||
print(f"\nBlue noise texture saved to: {output_path}")
|
||||
print(f"Size: {args.width}x{args.height}")
|
||||
print(f"Method: {args.method}")
|
||||
|
||||
fft = np.fft.fft2(noise)
|
||||
power_spectrum = np.abs(np.fft.fftshift(fft)) ** 2
|
||||
print(f"Power spectrum range: {power_spectrum.min():.2e} - {power_spectrum.max():.2e}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user