Created
January 26, 2026 19:12
-
-
Save NoRaincheck/1f3c302cc7ca382e6e19bae2e2383ed4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| FFT Terrain Generator | |
| Generates 2D fractal noise terrain using Fast Fourier Transforms. | |
| Based on Paul Bourke's "Frequency Synthesis of Landscapes (and clouds)" (1997). | |
| """ | |
| import argparse | |
| import numpy as np | |
| from PIL import Image | |
| def generate_white_noise(size: int, rng: np.random.RandomState) -> np.ndarray: | |
| """Generate a 2D array of white noise (random values).""" | |
| return rng.random((size, size)) | |
| def create_frequency_filter(size: int, roughness: float) -> np.ndarray: | |
| """ | |
| Create a frequency filter mask for 1/f^r scaling. | |
| The filter applies 1/f^r scaling where: | |
| - f is the frequency (distance from center in frequency space) | |
| - r is the roughness factor | |
| Args: | |
| size: Size of the 2D array (must be square) | |
| roughness: The roughness exponent (typically 2.0-3.0 for natural terrain) | |
| Returns: | |
| 2D array of filter values | |
| """ | |
| # Create frequency coordinates centered at (0, 0) | |
| freq_x = np.fft.fftfreq(size) | |
| freq_y = np.fft.fftfreq(size) | |
| # Create 2D frequency grid | |
| fx, fy = np.meshgrid(freq_x, freq_y) | |
| # Calculate frequency magnitude (distance from DC component) | |
| frequency = np.sqrt(fx**2 + fy**2) | |
| # Avoid division by zero at DC component | |
| frequency[0, 0] = 1.0 | |
| # Apply 1/f^r filter | |
| filter_mask = 1.0 / (frequency ** roughness) | |
| # Set DC component to 0 to center the output around zero mean | |
| filter_mask[0, 0] = 0.0 | |
| return filter_mask | |
| def generate_fft_terrain(size: int, roughness: float, seed: int | None = None) -> np.ndarray: | |
| """ | |
| Generate fractal terrain using FFT noise synthesis. | |
| Algorithm: | |
| 1. Generate white noise | |
| 2. Apply 2D FFT | |
| 3. Scale by frequency filter (1/f^r) | |
| 4. Apply inverse FFT | |
| Args: | |
| size: Size of the terrain (size x size pixels) | |
| roughness: Roughness factor (typically 2.0-3.0) | |
| seed: Optional random seed for reproducibility | |
| Returns: | |
| 2D array of terrain heights (normalized to 0-1) | |
| """ | |
| rng = np.random.RandomState(seed) | |
| # Step 1: Generate white noise | |
| noise = generate_white_noise(size, rng) | |
| # Step 2: Apply 2D FFT | |
| fft_result = np.fft.fft2(noise) | |
| # Step 3: Create and apply frequency filter | |
| frequency_filter = create_frequency_filter(size, roughness) | |
| filtered_fft = fft_result * frequency_filter | |
| # Step 4: Apply inverse FFT and take real part | |
| terrain = np.fft.ifft2(filtered_fft).real | |
| # Normalize to 0-1 range | |
| terrain = (terrain - terrain.min()) / (terrain.max() - terrain.min()) | |
| return terrain | |
| def save_terrain_image(terrain: np.ndarray, filename: str) -> None: | |
| """ | |
| Save terrain data as a grayscale PNG image. | |
| Args: | |
| terrain: 2D array of terrain heights (normalized to 0-1) | |
| filename: Output filename | |
| """ | |
| # Convert to 8-bit grayscale (0-255) | |
| terrain_uint8 = (terrain * 255).astype(np.uint8) | |
| # Create PIL image and save | |
| image = Image.fromarray(terrain_uint8, mode='L') | |
| image.save(filename) | |
| print(f"Saved terrain image to: {filename}") | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Generate fractal terrain using FFT noise synthesis" | |
| ) | |
| parser.add_argument( | |
| "-s", "--size", | |
| type=int, | |
| default=512, | |
| help="Size of the terrain (must be power of 2, default: 512)" | |
| ) | |
| parser.add_argument( | |
| "-r", "--roughness", | |
| type=float, | |
| default=2.4, | |
| help="Roughness factor (typically 2.0-3.0, default: 2.4)" | |
| ) | |
| parser.add_argument( | |
| "-o", "--output", | |
| type=str, | |
| default="terrain.png", | |
| help="Output filename (default: terrain.png)" | |
| ) | |
| parser.add_argument( | |
| "--seed", | |
| type=int, | |
| default=None, | |
| help="Random seed for reproducibility" | |
| ) | |
| args = parser.parse_args() | |
| # Validate size is power of 2 | |
| if args.size & (args.size - 1) != 0 or args.size <= 0: | |
| print(f"Warning: Size {args.size} is not a power of 2. FFT works best with powers of 2.") | |
| print(f"Generating {args.size}x{args.size} terrain with roughness={args.roughness}") | |
| # Generate terrain | |
| terrain = generate_fft_terrain(args.size, args.roughness, args.seed) | |
| # Save image | |
| save_terrain_image(terrain, args.output) | |
| print("Done!") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment