├── assets └── terrain_zigger.gif ├── LICENSE ├── README.md └── terrain_zigger.zig /assets/terrain_zigger.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JosefAlbers/TerrainZigger/main/assets/terrain_zigger.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 JosefAlbers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TerrainZigger 2 | 3 | TerrainZigger is a 3D terrain generator written in Zig using the Raylib library. It creates procedurally generated landscapes with dynamic water features, offering an interactive 3D visualization. 4 | 5 | ![TerrainZigger](https://raw.githubusercontent.com/JosefAlbers/TerrainZigger/main/assets/terrain_zigger.gif) 6 | 7 | ## Features 8 | 9 | - Procedural terrain generation using Fractional Brownian Motion (FBM) 10 | - Real-time 3D rendering with Raylib 11 | - Interactive camera controls for exploring the terrain 12 | - Dynamic water level adjustment 13 | - On-the-fly terrain regeneration 14 | - Customizable terrain parameters 15 | 16 | ## Prerequisites 17 | 18 | To build and run TerrainZigger, you'll need: 19 | 20 | - [Zig](https://ziglang.org/) (version 0.13.0 recommended) 21 | - [Raylib](https://www.raylib.com/) 22 | 23 | ## Building and Running 24 | 25 | 1. Clone the repository: 26 | ``` 27 | git clone https://github.com/JosefAlbers/TerrainZigger.git 28 | cd TerrainZigger 29 | ``` 30 | 31 | 2. Build the project: 32 | ``` 33 | zig build-exe terrain_zigger.zig -lc $(pkg-config --libs --cflags raylib) 34 | ``` 35 | 36 | 3. Run the executable: 37 | ``` 38 | ./terrain_zigger 39 | ``` 40 | 41 | ## Controls 42 | 43 | - **R** or **Right Mouse Button**: Regenerate terrain 44 | - **Left Mouse Button**: Rotate camera 45 | - **Mouse Wheel**: Zoom in/out 46 | - **Z**: Reset camera 47 | - **, (Comma)**: Decrease water level 48 | - **. (Period)**: Increase water level 49 | 50 | ## Customization 51 | 52 | You can adjust various parameters in the `terrain_zigger.zig` file to customize the terrain generation: 53 | 54 | - `TERRAIN_SIZE`: Changes the size of the terrain grid 55 | - `INITIAL_WATER_LEVEL`: Sets the initial height of the water plane 56 | - `CUBE_SIZE`: Modifies the size of individual terrain cubes 57 | 58 | Feel free to experiment with the `fbm` function parameters in `generateTerrain` to create different terrain styles. 59 | 60 | ## Contributing 61 | 62 | Contributions are welcome! Please feel free to submit a Pull Request. 63 | 64 | ## License 65 | 66 | This project is open source and available under the [MIT License](LICENSE). 67 | 68 | ## Acknowledgments 69 | 70 | - Terrain generation algorithm inspired by [Perlin Noise](https://en.wikipedia.org/wiki/Perlin_noise) 71 | - 3D rendering made possible by [Raylib](https://www.raylib.com/) 72 | 73 | --- 74 | 75 | Happy terrain generating! 🏞️ -------------------------------------------------------------------------------- /terrain_zigger.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const ray = @cImport({ 3 | @cInclude("raylib.h"); 4 | }); 5 | 6 | const WINDOW_WIDTH: i32 = 600; 7 | const WINDOW_HEIGHT: i32 = 450; 8 | const TERRAIN_SIZE: i32 = 128; 9 | const CUBE_SIZE: f32 = 1.0; 10 | const INITIAL_WATER_LEVEL: f32 = 5.0; 11 | 12 | fn fade(t: f32) f32 { 13 | return t * t * t * (t * (t * 6 - 15) + 10); 14 | } 15 | 16 | fn lerp(t: f32, a: f32, b: f32) f32 { 17 | return a + t * (b - a); 18 | } 19 | 20 | fn grad(hash: i32, x: f32, y: f32, z: f32) f32 { 21 | const h = hash & 15; 22 | const u = if (h < 8) x else y; 23 | const v = if (h < 4) y else if (h == 12 or h == 14) x else z; 24 | return (if ((h & 1) == 0) u else -u) + (if ((h & 2) == 0) v else -v); 25 | } 26 | 27 | fn noise(x: f32, y: f32, z: f32, p: []const i32) f32 { 28 | const X = @as(u8, @intFromFloat(x)) & 255; 29 | const Y = @as(u8, @intFromFloat(y)) & 255; 30 | const Z = @as(u8, @intFromFloat(z)) & 255; 31 | const x_floor = x - @floor(x); 32 | const y_floor = y - @floor(y); 33 | const z_floor = z - @floor(z); 34 | const u = fade(x_floor); 35 | const v = fade(y_floor); 36 | const w = fade(z_floor); 37 | 38 | const A = @as(u8, @intCast(p[X])) +% Y; 39 | const AA = @as(u8, @intCast(p[A])) +% Z; 40 | const AB = @as(u8, @intCast(p[A +% 1])) +% Z; 41 | const B = @as(u8, @intCast(p[X +% 1])) +% Y; 42 | const BA = @as(u8, @intCast(p[B])) +% Z; 43 | const BB = @as(u8, @intCast(p[B +% 1])) +% Z; 44 | 45 | return lerp(w, lerp(v, lerp(u, grad(p[@intCast(AA)], x_floor, y_floor, z_floor), grad(p[@intCast(BA)], x_floor - 1, y_floor, z_floor)), lerp(u, grad(p[@intCast(AB)], x_floor, y_floor - 1, z_floor), grad(p[@intCast(BB)], x_floor - 1, y_floor - 1, z_floor))), lerp(v, lerp(u, grad(p[@intCast(AA +% 1)], x_floor, y_floor, z_floor - 1), grad(p[@intCast(BA +% 1)], x_floor - 1, y_floor, z_floor - 1)), lerp(u, grad(p[@intCast(AB +% 1)], x_floor, y_floor - 1, z_floor - 1), grad(p[@intCast(BB +% 1)], x_floor - 1, y_floor - 1, z_floor - 1)))); 46 | } 47 | 48 | fn generatePermutation(allocator: std.mem.Allocator, seed: i32) ![]i32 { 49 | var rng = std.rand.DefaultPrng.init(@intCast(seed)); 50 | var random = rng.random(); 51 | 52 | const perm = try allocator.alloc(i32, 512); 53 | var source = try allocator.alloc(i32, 256); 54 | defer allocator.free(source); 55 | 56 | for (source, 0..) |*value, index| { 57 | value.* = @intCast(index); 58 | } 59 | 60 | var i: usize = 255; 61 | while (i > 0) : (i -= 1) { 62 | const j = random.intRangeAtMost(usize, 0, i); 63 | const temp = source[i]; 64 | source[i] = source[j]; 65 | source[j] = temp; 66 | } 67 | 68 | for (perm, 0..) |*value, index| { 69 | value.* = source[index % 256]; 70 | } 71 | 72 | return perm; 73 | } 74 | 75 | fn fbm(x: f32, y: f32, octaves: u32, persistence: f32, lacunarity: f32, scale: f32, p: []const i32) f32 { 76 | var value: f32 = 0; 77 | var amplitude: f32 = 1; 78 | var frequency: f32 = 1; 79 | 80 | var i: u32 = 0; 81 | while (i < octaves) : (i += 1) { 82 | value += amplitude * noise(x * frequency / scale, y * frequency / scale, 0, p); 83 | amplitude *= persistence; 84 | frequency *= lacunarity; 85 | } 86 | 87 | return value; 88 | } 89 | 90 | fn generateTerrain(allocator: std.mem.Allocator, seed: i32) ![]f32 { 91 | var terrain = try allocator.alloc(f32, @as(usize, TERRAIN_SIZE * TERRAIN_SIZE)); 92 | const perm = try generatePermutation(allocator, seed); 93 | defer allocator.free(perm); 94 | 95 | for (0..TERRAIN_SIZE) |y| { 96 | for (0..TERRAIN_SIZE) |x| { 97 | terrain[y * TERRAIN_SIZE + x] = fbm(@floatFromInt(x), @floatFromInt(y), 6, 0.5, 2.0, 50.0, perm); 98 | } 99 | } 100 | 101 | // Normalize the terrain 102 | var min: f32 = terrain[0]; 103 | var max: f32 = terrain[0]; 104 | for (terrain) |value| { 105 | min = @min(min, value); 106 | max = @max(max, value); 107 | } 108 | 109 | for (terrain) |*value| { 110 | value.* = (value.* - min) / (max - min); 111 | } 112 | 113 | return terrain; 114 | } 115 | 116 | pub fn main() !void { 117 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 118 | defer _ = gpa.deinit(); 119 | const allocator = gpa.allocator(); 120 | 121 | ray.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "JosefAlbers/TerrainZigger"); 122 | defer ray.CloseWindow(); 123 | 124 | var camera_distance: f32 = 200.0; 125 | var camera_angle = ray.Vector2{ .x = 0.7, .y = 0.7 }; 126 | var camera = ray.Camera3D{ 127 | .position = .{ 128 | .x = @cos(camera_angle.y) * @cos(camera_angle.x) * camera_distance, 129 | .y = @sin(camera_angle.x) * camera_distance, 130 | .z = @sin(camera_angle.y) * @cos(camera_angle.x) * camera_distance, 131 | }, 132 | .target = .{ .x = 0.0, .y = -20.0, .z = 0.0 }, 133 | .up = .{ .x = 0.0, .y = 1.0, .z = 0.0 }, 134 | .fovy = 45.0, 135 | .projection = ray.CAMERA_PERSPECTIVE, 136 | }; 137 | 138 | var terrain = try generateTerrain(allocator, @intFromFloat(ray.GetTime() * 1000000.0)); 139 | defer allocator.free(terrain); 140 | 141 | var water_level: f32 = INITIAL_WATER_LEVEL; 142 | 143 | ray.SetTargetFPS(60); 144 | 145 | while (!ray.WindowShouldClose()) { 146 | // Camera rotation 147 | if (ray.IsMouseButtonDown(ray.MOUSE_BUTTON_LEFT)) { 148 | const delta = ray.GetMouseDelta(); 149 | camera_angle.x += delta.y * 0.003; 150 | camera_angle.y -= delta.x * 0.003; 151 | camera_angle.x = std.math.clamp(camera_angle.x, -std.math.pi / 3.0, std.math.pi / 3.0); 152 | } 153 | 154 | // Camera zoom 155 | const wheel = ray.GetMouseWheelMove(); 156 | if (wheel != 0) { 157 | camera_distance *= (1.0 - wheel * 0.02); 158 | camera_distance = std.math.clamp(camera_distance, 10.0, 300.0); 159 | } 160 | 161 | // Update camera position 162 | camera.position = .{ 163 | .x = @cos(camera_angle.y) * @cos(camera_angle.x) * camera_distance, 164 | .y = @sin(camera_angle.x) * camera_distance, 165 | .z = @sin(camera_angle.y) * @cos(camera_angle.x) * camera_distance, 166 | }; 167 | 168 | // Reset camera 169 | if (ray.IsKeyPressed(ray.KEY_Z)) { 170 | camera_angle = .{ .x = 0.7, .y = 0.7 }; 171 | camera_distance = 200.0; 172 | } 173 | 174 | // Regenerate terrain 175 | if (ray.IsKeyPressed(ray.KEY_R) or ray.IsMouseButtonPressed(ray.MOUSE_BUTTON_RIGHT)) { 176 | allocator.free(terrain); 177 | terrain = try generateTerrain(allocator, @intFromFloat(ray.GetTime() * 1000000.0)); 178 | } 179 | 180 | // Adjust water level 181 | if (ray.IsKeyDown(ray.KEY_COMMA)) water_level = @max(0.0, water_level - 0.1); 182 | if (ray.IsKeyDown(ray.KEY_PERIOD)) water_level += 0.1; 183 | 184 | // Drawing 185 | ray.BeginDrawing(); 186 | defer ray.EndDrawing(); 187 | 188 | ray.ClearBackground(ray.RAYWHITE); 189 | ray.BeginMode3D(camera); 190 | 191 | // Draw terrain 192 | for (0..TERRAIN_SIZE) |z| { 193 | for (0..TERRAIN_SIZE) |x| { 194 | const height = terrain[z * TERRAIN_SIZE + x]; 195 | const cube_height = height * 20.0; 196 | const pos = ray.Vector3{ 197 | .x = @as(f32, @floatFromInt(x)) * CUBE_SIZE - @as(f32, @floatFromInt(TERRAIN_SIZE)) * CUBE_SIZE / 2, 198 | .y = cube_height / 2, 199 | .z = @as(f32, @floatFromInt(z)) * CUBE_SIZE - @as(f32, @floatFromInt(TERRAIN_SIZE)) * CUBE_SIZE / 2, 200 | }; 201 | var color = ray.ColorFromHSV(120 * height, 0.8, 0.8); 202 | if (cube_height < water_level * 2) { 203 | color.r = @intFromFloat(@as(f32, @floatFromInt(color.r)) * 0.7); 204 | color.g = @intFromFloat(@as(f32, @floatFromInt(color.g)) * 0.7); 205 | color.b = @intFromFloat(@as(f32, @floatFromInt(color.b)) * 0.7); 206 | } 207 | ray.DrawCube(pos, CUBE_SIZE, cube_height, CUBE_SIZE, color); 208 | } 209 | } 210 | 211 | // Draw water 212 | const water_size = @as(f32, @floatFromInt(TERRAIN_SIZE)) * CUBE_SIZE; 213 | const water_pos = ray.Vector3{ .x = 0, .y = water_level, .z = 0 }; 214 | ray.DrawCube(water_pos, water_size, 0.1, water_size, ray.ColorAlpha(ray.SKYBLUE, 0.5)); 215 | 216 | ray.DrawGrid(10, 10.0); 217 | ray.EndMode3D(); 218 | 219 | // Draw UI 220 | ray.DrawRectangle(10, 10, 245, 162, ray.Fade(ray.SKYBLUE, 0.5)); 221 | ray.DrawRectangleLines(10, 10, 245, 162, ray.BLUE); 222 | ray.DrawText("Controls:", 20, 20, 10, ray.BLACK); 223 | ray.DrawText("- R or Right Mouse: Regenerate terrain", 40, 40, 10, ray.DARKGRAY); 224 | ray.DrawText("- , or .: Decrease/Increase water level", 40, 55, 10, ray.DARKGRAY); 225 | ray.DrawText("- Left Mouse: Rotate camera", 40, 70, 10, ray.DARKGRAY); 226 | ray.DrawText("- Mouse Wheel: Zoom in/out", 40, 85, 10, ray.DARKGRAY); 227 | ray.DrawText("- Z: Reset camera", 40, 100, 10, ray.DARKGRAY); 228 | ray.DrawText(ray.TextFormat("Camera Angle X: %.2f", camera_angle.x), 20, 120, 10, ray.DARKGRAY); 229 | ray.DrawText(ray.TextFormat("Camera Angle Y: %.2f", camera_angle.y), 20, 135, 10, ray.DARKGRAY); 230 | ray.DrawText(ray.TextFormat("Camera Distance: %.2f", camera_distance), 20, 150, 10, ray.DARKGRAY); 231 | ray.DrawFPS(10, 180); 232 | } 233 | } 234 | --------------------------------------------------------------------------------