├── .gitignore ├── .vscode └── tasks.json ├── README.md ├── assets ├── dagif.gif └── screenshot.png ├── build.zig ├── build.zig.zon ├── shaders ├── brush.comp ├── draw.comp ├── funcs.glsl ├── shared.glsl ├── sim.comp └── worldgen.comp └── src ├── app.zig ├── gl45.zig ├── graphics.zig ├── main.zig └── material.zig /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build and run", 8 | "type": "shell", 9 | "command": "zig build run", 10 | "problemMatcher": [ 11 | "$zig" 12 | ] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

dustpile

3 | GPU falling sand sim in Zig 4 |
5 |
6 | 7 | Uses [generated OpenGL bindings](https://github.com/MasterQ32/zig-opengl) to interact with the GPU. 8 | Requires zig master to compile 9 | 10 | ``` 11 | - B : Change brush type 12 | - W : Reset world 13 | - Space : Pause / Resume simulation 14 | - P : Step simulation 15 | - 1-9 : Switch to element 16 | ``` 17 | 18 | ![a](assets/dagif.gif) 19 | ![](assets/screenshot.png) -------------------------------------------------------------------------------- /assets/dagif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Game4all/dustpile/ee54b606fca18c78e07d8cd0363984ec46ebd7e6/assets/dagif.gif -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Game4all/dustpile/ee54b606fca18c78e07d8cd0363984ec46ebd7e6/assets/screenshot.png -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mach_glfw = @import("mach_glfw"); 3 | 4 | pub fn build(b: *std.Build) void { 5 | const target = b.standardTargetOptions(.{}); 6 | const optimize = b.standardOptimizeOption(.{}); 7 | 8 | const exe = b.addExecutable(.{ 9 | .name = "dustpile", 10 | .root_source_file = b.path("src/main.zig"), 11 | .target = target, 12 | .optimize = optimize, 13 | }); 14 | 15 | //glfw dependency 16 | const glfw = b.dependency("mach_glfw", .{ 17 | .target = target, 18 | .optimize = optimize, 19 | }); 20 | 21 | exe.root_module.addImport("glfw", glfw.module("mach-glfw")); 22 | 23 | b.installArtifact(exe); 24 | const run_cmd = b.addRunArtifact(exe); 25 | run_cmd.step.dependOn(b.getInstallStep()); 26 | 27 | if (b.args) |args| { 28 | run_cmd.addArgs(args); 29 | } 30 | 31 | const run_step = b.step("run", "Run the app"); 32 | run_step.dependOn(&run_cmd.step); 33 | } 34 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "dustpile", 3 | .version = "0.0.1", 4 | .dependencies = .{ 5 | .mach_glfw = .{ 6 | .url = "git+https://github.com/hexops/mach-glfw#affdd6ae6f2ac2c3b9162784bdad345c561eeeea", 7 | .hash = "122022ea6df16700e521078c20d7d01f894c6f967e6c6ce1ea166426b4fc61667de3", 8 | }, 9 | }, 10 | .paths = .{ 11 | "src", 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /shaders/brush.comp: -------------------------------------------------------------------------------- 1 | #version 450 2 | #include shaders/shared.glsl 3 | 4 | layout(local_size_x = 32, local_size_y = 32) in; 5 | 6 | layout(rgba8i, binding = 0) uniform iimage2D worldTex; 7 | 8 | void main() { 9 | ivec2 pixelCoords = ivec2(gl_GlobalInvocationID.xy); 10 | ivec2 imageSize = imageSize(worldTex); 11 | 12 | if (pixelCoords.x >= imageSize.x || pixelCoords.y >= imageSize.y || inputState == 0) 13 | return; 14 | 15 | // prevent the brush from replacing existing non-empty cells unless the current material is air (0) 16 | ivec4 cell = imageLoad(worldTex, pixelCoords / PIXEL_SIZE); 17 | if (cell.r != 0 && CurrentMaterial != MAT_ID_AIR) 18 | return; 19 | 20 | ivec2 delta = pixelCoords - BrushPos; 21 | 22 | //todo: add more brush shapes? 23 | if (Brush(delta, BrushSize) && (inputState & 1) == 1) { 24 | int rnd = (int(gl_LocalInvocationIndex) + int(Time * 1000.)) % 255; 25 | imageStore(worldTex, pixelCoords / PIXEL_SIZE, ivec4(CurrentMaterial, 0, 0, CurrentMaterial != MAT_ID_AIR ? rnd : 0)); 26 | } 27 | } -------------------------------------------------------------------------------- /shaders/draw.comp: -------------------------------------------------------------------------------- 1 | #version 450 2 | #include shaders/shared.glsl 3 | 4 | layout(local_size_x = 32, local_size_y = 32) in; 5 | 6 | layout(rgba8, binding = 0) writeonly uniform image2D frame; 7 | layout(rgba8i, binding = 1) readonly uniform iimage2D worldTex; 8 | 9 | // translate a point then rotate it around the translation point 10 | vec2 sdTranslateRotate(vec2 p, vec2 t, float a) { 11 | float c = cos(a), s = sin(a); 12 | mat2 m = mat2(c, -s, s, c); 13 | return m * (p - t); 14 | } 15 | 16 | // hexagon sdf 17 | float sdHexagon( in vec2 p, in float r ) 18 | { 19 | const vec3 k = vec3(-0.866025404,0.5,0.577350269); 20 | p = abs(p); 21 | p -= 2.0 * min(dot(k.xy,p), 0.0) * k.xy; 22 | p -= vec2(clamp(p.x, -k.z*r, k.z*r), r); 23 | return length(p) * sign(p.y); 24 | } 25 | 26 | // box sdf 27 | float boxSDF(ivec2 pos, ivec2 dim, ivec2 origin) { 28 | const vec2 cords = dim; 29 | vec2 d = abs(pos - origin) - cords; 30 | return length(max(d,0.0)) + min(max(d.x,d.y),0.0); 31 | } 32 | 33 | // Draw the brush preview over the world 34 | void drawBrush(ivec2 pos, inout vec4 finalColor) { 35 | ivec2 delta = (pos - BrushPos) / PIXEL_SIZE; 36 | if (Brush(delta * PIXEL_SIZE, BrushSize)) { 37 | finalColor = mix(finalColor, vec4(1., 1., 1., 1.), 0.5); 38 | } 39 | } 40 | 41 | // Draw the state of the simulation if it is paused 42 | void drawTimeStateUI(ivec2 pos, inout vec4 finalColor) { 43 | const ivec2 frameSize = imageSize(frame); 44 | const float blinkTime = abs(sin(Time * 5.)); 45 | if (SimRunning == 0 && blinkTime > 0.5) { 46 | const ivec2 pauseIconOrigin = ivec2(18, frameSize.y / PIXEL_SIZE - 12); 47 | const float boxes = min(boxSDF(pos / PIXEL_SIZE, ivec2(2, 4), pauseIconOrigin), boxSDF(pos / PIXEL_SIZE, ivec2(2, 4), pauseIconOrigin + ivec2(4, 0))); 48 | if (boxes < 0.) { 49 | finalColor = mix(finalColor, vec4(1.), 0.9); 50 | } else if (boxes <= 1.) { 51 | finalColor = mix(finalColor, vec4(0.), 0.8); 52 | } 53 | } 54 | } 55 | 56 | void drawMaterialSelectionUI(ivec2 pos, inout vec4 finalColor) { 57 | const ivec2 frameSize = imageSize(frame); 58 | const vec2 materialIconOrigin = vec2(8, frameSize.y / PIXEL_SIZE - 12); 59 | 60 | float dist = sdHexagon(sdTranslateRotate(vec2(pos / PIXEL_SIZE), materialIconOrigin, 3.14 / 2.), 4.); 61 | 62 | if (dist < 0.3) { 63 | const vec4 coll = GetMaterialColor(pos / PIXEL_SIZE, ivec4(CurrentMaterial, 0, 0, length(int(abs(Random(vec2(pos / PIXEL_SIZE)) * 100.)) % 255))); 64 | finalColor = mix(finalColor, coll, 1.); 65 | } else if (dist <= 1.2) { 66 | finalColor = mix(finalColor, vec4(0.), 0.8); 67 | } 68 | } 69 | 70 | 71 | void main() { 72 | uvec2 resolution = imageSize(frame); 73 | uvec2 pixel = gl_GlobalInvocationID.xy; 74 | 75 | if (pixel.x >= resolution.x || pixel.y >= resolution.y) 76 | return; 77 | 78 | ivec4 pixelS = imageLoad(worldTex, ivec2(pixel) / PIXEL_SIZE); 79 | vec4 baseColor = GetMaterialColor(ivec2(pixel) / PIXEL_SIZE, pixelS); 80 | 81 | vec4 finalColor = vec4(baseColor); 82 | drawBrush(ivec2(pixel), finalColor); 83 | drawTimeStateUI(ivec2(pixel), finalColor); 84 | drawMaterialSelectionUI(ivec2(pixel), finalColor); 85 | 86 | finalColor = pow(finalColor, vec4(2.2)); 87 | 88 | imageStore(frame, ivec2(pixel), finalColor); 89 | } 90 | 91 | -------------------------------------------------------------------------------- /shaders/funcs.glsl: -------------------------------------------------------------------------------- 1 | 2 | // Returns the cell at the given position, or a cell with r = 255 if the position is out of bounds 3 | ivec4 GetCell(ivec2 pos) { 4 | const ivec2 resolution = imageSize(World) / PIXEL_SIZE; 5 | if (pos.x < 0 || pos.x >= resolution.x || pos.y < 0 || pos.y >= resolution.y) 6 | return ivec4(MAT_ID_WALL, 0, 0, 0); 7 | else 8 | return imageLoad(World, pos); 9 | } 10 | 11 | /// Swaps the contents of two cells 12 | void SwapCells(ivec2 src, ivec4 srcCell, ivec2 dst, ivec4 dstCell) { 13 | imageStore(FutureWorld, dst, srcCell); 14 | imageStore(FutureWorld, src, dstCell); 15 | } 16 | 17 | bool SimulateSolidCell(ivec2 pos, ivec4 cellValue) { 18 | ivec2 underCellPos = pos + ivec2(0, -1); 19 | ivec4 underCell = GetCell(underCellPos); 20 | 21 | // swap with under cell if it's empty 22 | if (underCell.r == 0 || underCell.r == MAT_ID_WATER) { 23 | SwapCells(pos, cellValue, underCellPos, underCell); 24 | return true; 25 | } 26 | 27 | // swap with under right or left cell if it's empty 28 | ivec2 randomSideCell = ivec2(pos) + ivec2(RandomDir(vec2(pos) + Time), -1); 29 | ivec4 sideCell = GetCell(randomSideCell); 30 | 31 | if (sideCell.r == 0 || sideCell.r == MAT_ID_WATER) { 32 | SwapCells(pos, cellValue, randomSideCell, sideCell); 33 | return true; 34 | } 35 | 36 | return false; 37 | } 38 | 39 | bool SimulateLiquidCell(ivec2 pos, ivec4 cellValue) { 40 | ivec2 randomSideCell = ivec2(pos) + ivec2(RandomDir(vec2(pos) + vec2(gl_LocalInvocationIndex) + Time), 0); 41 | ivec4 sideCell = GetCell(randomSideCell); 42 | 43 | ivec2 underCellPos = pos + ivec2(0, -1); 44 | ivec4 underCell = GetCell(underCellPos); 45 | 46 | if (sideCell.r == 0 && underCell.r == 0) { 47 | SwapCells(pos, cellValue, underCellPos, underCell); 48 | return true; 49 | } else if (sideCell.r == 0) { 50 | SwapCells(pos, cellValue, randomSideCell, sideCell); 51 | return true; 52 | } else if (underCell.r == 0) { 53 | SwapCells(pos, cellValue, underCellPos, underCell); 54 | return true; 55 | } 56 | 57 | // swap with under right or left cell if it's empty 58 | ivec2 randomUSideCell = ivec2(pos) + ivec2(RandomDir(vec2(pos) + vec2(gl_LocalInvocationIndex) + Time), -1); 59 | ivec4 UsideCell = GetCell(randomUSideCell); 60 | 61 | if (UsideCell.r == 0) { 62 | SwapCells(pos, cellValue, randomUSideCell, UsideCell); 63 | return true; 64 | } 65 | return false; 66 | } 67 | 68 | bool SimulateDirtCell(ivec2 pos, inout ivec4 cell) { 69 | if (SimulateSolidCell(pos, ivec4(cell.r, 0, cell.b, cell.a))) 70 | return true; 71 | 72 | ivec4 aboveCell = GetCell(pos + ivec2(0, 1)); 73 | 74 | if (aboveCell.r == MAT_ID_AIR && cell.g != 2) 75 | { 76 | if (Random(vec2(pos) + Time) > 0.999) 77 | cell.g = 1; 78 | } 79 | else if (aboveCell.r == MAT_ID_WATER) 80 | { 81 | if (Random(vec2(pos) + Time) > 0.999) 82 | cell.g = 2; 83 | } 84 | else 85 | cell.g = 0; 86 | return false; 87 | } 88 | 89 | bool SimulateAcidCell(ivec2 pos, inout ivec4 cell) { 90 | ivec2 underCellPos = pos + ivec2(0, -1); 91 | ivec4 underCell = GetCell(underCellPos); 92 | 93 | if (underCell.r != MAT_ID_ACID && underCell.r != MAT_ID_AIR && underCell.r != MAT_ID_WALL) { 94 | imageStore(FutureWorld, underCellPos, ivec4(MAT_ID_AIR, 0, 0, 0)); 95 | return true; 96 | } 97 | 98 | ivec2 randomSidePos = ivec2(pos) + ivec2(RandomDir(vec2(pos) + vec2(gl_LocalInvocationIndex) + Time), 0); 99 | ivec4 randomSideCell = GetCell(randomSidePos); 100 | 101 | if (randomSideCell.r != MAT_ID_ACID && randomSideCell.r != MAT_ID_AIR && randomSideCell.r != MAT_ID_WALL) { 102 | imageStore(FutureWorld, randomSidePos, ivec4(MAT_ID_AIR, 0, 0, 0)); 103 | return true; 104 | } 105 | 106 | ivec2 upCellPos = pos + ivec2(0, 1); 107 | ivec4 upCell = GetCell(upCellPos); 108 | 109 | if (upCell.r != MAT_ID_ACID && upCell.r != MAT_ID_AIR && upCell.r != MAT_ID_WALL) { 110 | imageStore(FutureWorld, upCellPos, ivec4(MAT_ID_AIR, 0, 0, 0)); 111 | return true; 112 | } 113 | 114 | if (SimulateLiquidCell(pos, ivec4(cell.r, 0, cell.b, cell.a))) 115 | return true; 116 | 117 | return false; 118 | } 119 | 120 | bool SimulateMossCell(ivec2 pos, inout ivec4 cell) { 121 | 122 | if (Random(vec2(pos) + Time) > 0.997) { 123 | ivec2 underCellPos = pos + ivec2(0, -1); 124 | ivec4 underCell = GetCell(underCellPos); 125 | 126 | if (underCell.r == MAT_ID_AIR) { 127 | //imageStore(FutureWorld, underCellPos, ivec4(MAT_ID_MOSS, cell.g, cell.b + 1, int(abs(Random(vec2(pos) + Time + vec2(gl_LocalInvocationIndex)) * 10000.)) % 255)); 128 | if (cell.g == 0) { 129 | cell.g = int(abs(Random(vec2(pos) + Time + vec2(gl_LocalInvocationIndex))) * 100.) % 16; 130 | return false; 131 | } else if (cell.g > cell.b) { 132 | imageStore(FutureWorld, underCellPos, ivec4(MAT_ID_MOSS, cell.g, cell.b + 1, int(Random(vec2(pos) + Time + vec2(gl_LocalInvocationIndex)) * 100.) % 255)); 133 | return true; 134 | } 135 | } 136 | 137 | return false; 138 | } 139 | } 140 | 141 | bool SimulateLavaCell(ivec2 pos, inout ivec4 cell) { 142 | 143 | if (SimulateLiquidCell(pos, ivec4(cell.r, 0, cell.b, cell.a))) 144 | return true; 145 | 146 | 147 | if (Random(vec2(pos) + Time) > 0.9979) { 148 | imageStore(FutureWorld, pos, ivec4(MAT_ID_STONE, cell.g, cell.b, cell.a)); 149 | return true; 150 | } 151 | 152 | ivec2 underCellPos = pos + ivec2(0, -1); 153 | ivec4 underCell = GetCell(underCellPos); 154 | 155 | if (underCell.r == MAT_ID_WATER) { 156 | imageStore(FutureWorld, pos, ivec4(MAT_ID_STONE, cell.g, cell.b, cell.a)); 157 | return true; 158 | } 159 | 160 | ivec2 randomSidePos = ivec2(pos) + ivec2(RandomDir(vec2(pos) + vec2(gl_LocalInvocationIndex) + Time), 0); 161 | ivec4 randomSideCell = GetCell(randomSidePos); 162 | 163 | if (randomSideCell.r == MAT_ID_WATER) { 164 | imageStore(FutureWorld, pos, ivec4(MAT_ID_STONE, cell.g, cell.b, cell.a)); 165 | return true; 166 | } 167 | 168 | ivec2 upCellPos = pos + ivec2(0, 1); 169 | ivec4 upCell = GetCell(upCellPos); 170 | 171 | if (upCell.r == MAT_ID_WATER) { 172 | imageStore(FutureWorld, pos, ivec4(MAT_ID_STONE, cell.g, cell.b, cell.a)); 173 | return true; 174 | } 175 | 176 | return false; 177 | } -------------------------------------------------------------------------------- /shaders/shared.glsl: -------------------------------------------------------------------------------- 1 | 2 | #define PIXEL_SIZE 8 3 | 4 | // materials 5 | #define MAT_ID_AIR 0 6 | #define MAT_ID_SAND 1 7 | #define MAT_ID_WALL 2 8 | #define MAT_ID_DIRT 3 9 | #define MAT_ID_WATER 4 10 | #define MAT_ID_STONE 5 11 | #define MAT_ID_ACID 6 12 | #define MAT_ID_MOSS 7 13 | #define MAT_ID_LAVA 8 14 | 15 | 16 | // brush types 17 | #define BRUSH_TYPE_CIRCLE 0 18 | #define BRUSH_TYPE_SQUARE 1 19 | #define BRUSH_TYPE_HLINE 2 20 | 21 | // globals 22 | layout(std140, binding = 3) uniform globals { 23 | ivec2 BrushPos; 24 | float BrushSize; 25 | int BrushType; 26 | int CurrentMaterial; 27 | int inputState; 28 | float Time; 29 | int SimRunning; 30 | }; 31 | 32 | struct MaterialInfo { 33 | /// Base color of the material in RGBA integer format 34 | ivec4 BaseColor; 35 | float Density; 36 | }; 37 | 38 | 39 | layout(std140, binding = 4) uniform materials { 40 | MaterialInfo Materials[8]; 41 | }; 42 | 43 | MaterialInfo GetMaterialInfo(int id) { 44 | return Materials[id]; 45 | } 46 | 47 | // returns a random float between 0 and 1 48 | float Random(vec2 co){ 49 | return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453); 50 | } 51 | 52 | // returns a random direction offset of 1 or -1 53 | int RandomDir(vec2 co) { 54 | return Random(co) > 0.5 ? 1 : -1; 55 | } 56 | 57 | vec4 GetMaterialColor(ivec2 pos, ivec4 cell) { 58 | const MaterialInfo info = GetMaterialInfo(cell.r); 59 | switch (cell.r) { 60 | case MAT_ID_DIRT: 61 | switch (cell.g) { 62 | case 1: 63 | return vec4(65., 152., 10., 255.) / 255. + Random(vec2(cell.w)) * 0.2121; 64 | 65 | case 2: 66 | return vec4(0., 102., 10., 255.) / 255. + Random(vec2(cell.w)) * 0.2121; 67 | 68 | default: 69 | return vec4(info.BaseColor) / 255. + Random(vec2(cell.w)) * 0.2121; 70 | } 71 | break; 72 | 73 | case MAT_ID_WATER: 74 | return vec4(info.BaseColor) / 255. + 0.2121; 75 | break; 76 | 77 | case MAT_ID_LAVA: 78 | return vec4(info.BaseColor) / 255. + 0.2121 * max(0.7, abs(cos(Random(vec2(cell.w)) + Time))); 79 | break; 80 | 81 | default: 82 | return vec4(info.BaseColor) / 255. + Random(vec2(cell.w)) * 0.2121; 83 | } 84 | } 85 | 86 | 87 | 88 | /// SDF functions for the brush 89 | bool Brush(ivec2 pos, float size) { 90 | switch (BrushType) { 91 | case BRUSH_TYPE_CIRCLE: 92 | return length(pos) - size * PIXEL_SIZE < 0.; 93 | 94 | case BRUSH_TYPE_SQUARE: 95 | { 96 | const vec2 cords = ivec2(size * PIXEL_SIZE); 97 | vec2 d = abs(pos) - cords; 98 | return length(max(d,0.0)) + min(max(d.x,d.y),0.0) < 0.; 99 | } 100 | 101 | case BRUSH_TYPE_HLINE: { 102 | const vec2 cords = ivec2(size * PIXEL_SIZE, size * PIXEL_SIZE / 4); 103 | vec2 d = abs(pos) - cords; 104 | return length(max(d,0.0)) + min(max(d.x,d.y),0.0) < 0.; 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /shaders/sim.comp: -------------------------------------------------------------------------------- 1 | #version 450 2 | #include shaders/shared.glsl 3 | 4 | 5 | // workgroup size bigger than 1x1 here will cause access order issues with adjacent cells when trying to simulate complex patterns such as water 6 | layout(local_size_x = 1, local_size_y = 1) in; 7 | 8 | layout(rgba8i, binding = 0) uniform iimage2D World; 9 | layout(rgba8i, binding = 1) uniform iimage2D FutureWorld; 10 | 11 | #include shaders/funcs.glsl 12 | 13 | void main() { 14 | uvec2 resolution = imageSize(World) / PIXEL_SIZE; 15 | uvec2 pixel = gl_GlobalInvocationID.xy; 16 | 17 | if (pixel.x >= resolution.x || pixel.y >= resolution.y) 18 | return; 19 | 20 | ivec4 cell = GetCell(ivec2(pixel)); 21 | 22 | // if cell is empty or if simulation is paused, just copy the cell to the future world 23 | if (cell.r == 0 || SimRunning == 0) { 24 | imageStore(FutureWorld, ivec2(pixel), cell); 25 | return; 26 | } 27 | 28 | switch (cell.r) { 29 | // sand 30 | case MAT_ID_SAND: 31 | case MAT_ID_STONE: 32 | if (SimulateSolidCell(ivec2(pixel), cell)) 33 | return; 34 | break; 35 | 36 | // walls 37 | case MAT_ID_WALL: 38 | break; 39 | 40 | case MAT_ID_DIRT: 41 | if (SimulateDirtCell(ivec2(pixel), cell)) 42 | return; 43 | break; 44 | 45 | case MAT_ID_WATER: 46 | if (SimulateLiquidCell(ivec2(pixel), cell)) 47 | return; 48 | break; 49 | 50 | case MAT_ID_ACID: 51 | if (SimulateAcidCell(ivec2(pixel), cell)) 52 | return; 53 | break; 54 | 55 | case MAT_ID_MOSS: 56 | if (SimulateMossCell(ivec2(pixel), cell)) 57 | return; 58 | break; 59 | 60 | case MAT_ID_LAVA: 61 | if (SimulateLavaCell(ivec2(pixel), cell)) 62 | return; 63 | break; 64 | 65 | default: 66 | break; 67 | 68 | } 69 | imageStore(FutureWorld, ivec2(pixel), cell); 70 | } -------------------------------------------------------------------------------- /shaders/worldgen.comp: -------------------------------------------------------------------------------- 1 | #version 450 2 | #include shaders/shared.glsl 3 | 4 | layout(local_size_x = 32, local_size_y = 32) in; 5 | 6 | layout(rgba8i, binding = 0) uniform iimage2D World; 7 | 8 | // The height of the base layer of the world 9 | const int BASE_HEIGHT = 16; 10 | 11 | float lNoise(float pos) { 12 | return sin(pos * 0.001) * 0.8 13 | + Random(vec2(pos * 0.0001, 0.4783)) * 0.4 14 | + Random(vec2(pos * 0.0001, 0.4783)) * 0.8; 15 | } 16 | 17 | /// Returns the material ID for the given height 18 | int GetMaterialForHeight(ivec2 pos) { 19 | const int TARGET_HEIGHT = BASE_HEIGHT + int(lNoise(pos.x * 0.01) * 8.); 20 | const int DIRT_LAYER = BASE_HEIGHT - 3 + int(lNoise(pos.x * 0.01) * 2.); 21 | const int ROCK_LAYER = BASE_HEIGHT / 3 + int(lNoise(pos.x * 0.01) * 2.); 22 | const int SAND_LAYER = BASE_HEIGHT / 2 + int(lNoise(pos.x * 0.01) * 4.); 23 | 24 | if (pos.y >= TARGET_HEIGHT) 25 | return MAT_ID_AIR; 26 | else if (pos.y >= DIRT_LAYER) 27 | return MAT_ID_DIRT; 28 | else if (pos.y >= SAND_LAYER) 29 | return MAT_ID_SAND; 30 | else if (pos.y >= ROCK_LAYER) 31 | return MAT_ID_STONE; 32 | else 33 | return MAT_ID_STONE; 34 | 35 | return MAT_ID_AIR; 36 | } 37 | 38 | void main() { 39 | ivec2 size = imageSize(World) / PIXEL_SIZE; 40 | ivec2 pos = ivec2(gl_GlobalInvocationID.xy); 41 | 42 | if (pos.x >= size.x || pos.y >= size.y) 43 | return; 44 | 45 | int rnd = int(gl_LocalInvocationIndex) % 255; 46 | ivec4 material = ivec4(GetMaterialForHeight(pos), 0, 0, rnd); 47 | 48 | if (material.r == MAT_ID_AIR) 49 | material.a = 0; // Air is transparent 50 | 51 | imageStore(World, pos, material); 52 | } -------------------------------------------------------------------------------- /src/app.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const graphics = @import("graphics.zig"); 3 | const glfw = @import("glfw"); 4 | const materials = @import("material.zig"); 5 | 6 | const is_debug_build: bool = @import("builtin").mode == .Debug; 7 | 8 | /// The serializable application state. 9 | pub const ApplicationState = struct { 10 | brushPos: [2]i32, 11 | brushSize: f32, 12 | brushType: BrushType, 13 | material: i32, 14 | inputState: i32, 15 | time: f32, 16 | simRunning: i32, 17 | }; 18 | 19 | const BrushType = enum(i32) { circle = 0, square = 1, hline = 2 }; 20 | 21 | const SimRunState = enum(i32) { step = 2, running = 1, paused = 0 }; 22 | 23 | pub const Application = struct { 24 | allocator: std.mem.Allocator, 25 | window: glfw.Window, 26 | // render resources 27 | renderFramebuffer: graphics.Framebuffer, 28 | renderTexture: graphics.Texture, 29 | drawPipeline: graphics.ComputePipeline, 30 | // world simulation resources 31 | worldTexture: graphics.Texture, 32 | currentWorldImageIndex: u32 = 0, 33 | simPipeline: graphics.ComputePipeline, 34 | brushPipeline: graphics.ComputePipeline, 35 | worldGenPipeline: graphics.ComputePipeline, 36 | // global uniforms 37 | brushSize: i32 = 1, 38 | simState: SimRunState = .running, 39 | currentMaterial: i32 = 1, 40 | brushType: BrushType = .circle, 41 | globals: graphics.UniformBuffer(ApplicationState), 42 | materials: graphics.UniformBuffer(@TypeOf(materials.MATERIAL_LIST)), 43 | 44 | pub fn init(allocator: std.mem.Allocator) !@This() { 45 | const window = glfw.Window.create(800, 600, "dustpile", null, null, .{ 46 | .context_version_major = 4, 47 | .context_version_minor = 5, 48 | .srgb_capable = true, 49 | }); 50 | 51 | if (window == null) 52 | return error.WindowCreationFailed; 53 | 54 | try graphics.loadOpenGL(window.?); 55 | 56 | const worldTexture: graphics.Texture = .init(800, 600, graphics.gl.RGBA8I, 2); 57 | 58 | const renderTexture: graphics.Texture = .init(800, 600, graphics.gl.RGBA8, 1); 59 | const renderFramebuffer: graphics.Framebuffer = .init(&renderTexture); 60 | const drawPipeline: graphics.ComputePipeline = try .init("shaders/draw.comp", allocator); 61 | const brushPipeline: graphics.ComputePipeline = try .init("shaders/brush.comp", allocator); 62 | const simPipeline: graphics.ComputePipeline = try .init("shaders/sim.comp", allocator); 63 | const worldGenPipeline: graphics.ComputePipeline = try .init("shaders/worldgen.comp", allocator); 64 | 65 | var uniforms: graphics.UniformBuffer(ApplicationState) = .init(); 66 | uniforms.bind(drawPipeline.program, 3, "globals"); 67 | uniforms.bind(brushPipeline.program, 3, "globals"); 68 | uniforms.bind(simPipeline.program, 3, "globals"); 69 | uniforms.bind(worldGenPipeline.program, 3, "globals"); 70 | 71 | var smaterials: graphics.UniformBuffer(@TypeOf(materials.MATERIAL_LIST)) = .init(); 72 | smaterials.update(materials.MATERIAL_LIST); 73 | smaterials.bind(drawPipeline.program, 4, "materials"); 74 | smaterials.bind(brushPipeline.program, 4, "materials"); 75 | smaterials.bind(simPipeline.program, 4, "materials"); 76 | smaterials.bind(worldGenPipeline.program, 4, "materials"); 77 | 78 | // zig fmt: off 79 | return Application{ 80 | .window = window.?, 81 | .allocator = allocator, 82 | .renderFramebuffer = renderFramebuffer, 83 | .renderTexture = renderTexture, 84 | .drawPipeline = drawPipeline, 85 | .brushPipeline = brushPipeline, 86 | .simPipeline = simPipeline, 87 | .worldGenPipeline = worldGenPipeline, 88 | .worldTexture = worldTexture, 89 | .globals = uniforms, 90 | .materials = smaterials 91 | }; 92 | // zig fmt: onn 93 | } 94 | 95 | pub fn do_worldgen(app: *@This()) void { 96 | const size = app.window.getSize(); 97 | app.worldGenPipeline.use(); 98 | app.worldTexture.bind_image_layer(0, @intCast(app.currentWorldImageIndex), graphics.gl.WRITE_ONLY); 99 | app.worldGenPipeline.dispatch(size.width / 32, size.height / 32, 1); 100 | } 101 | 102 | pub fn reload_sim_shader(app: *@This()) void { 103 | const new_pipeline = graphics.ComputePipeline.init("shaders/sim.comp", app.allocator) catch |err| { 104 | std.log.err("Failed to reload sim shaders: {}", .{err}); 105 | return; 106 | }; 107 | app.simPipeline.deinit(); 108 | app.simPipeline = new_pipeline; 109 | app.globals.bind(app.simPipeline.program, 3, "globals"); 110 | app.materials.bind(app.simPipeline.program, 4, "materials"); 111 | 112 | std.log.info("Reloaded sim shaders", .{}); 113 | } 114 | 115 | /// Called when the window is resized. 116 | pub fn on_resize(win: glfw.Window, width: u32, height: u32) void { 117 | var app = win.getUserPointer(Application).?; 118 | 119 | // recreate the render texture and framebuffer 120 | app.renderFramebuffer.deinit(); 121 | app.renderTexture.deinit(); 122 | app.worldTexture.deinit(); 123 | app.worldTexture = .init(@intCast(width), @intCast(height), graphics.gl.RGBA8I, 2); 124 | app.renderTexture = .init(@intCast(width), @intCast(height), graphics.gl.RGBA8, 1); 125 | app.renderFramebuffer = .init(&app.renderTexture); 126 | 127 | graphics.gl.viewport(0, 0, @intCast(width), @intCast(height)); 128 | } 129 | 130 | pub fn on_scroll(win: glfw.Window, width: f64, height: f64) void { 131 | _ = width; 132 | var app = win.getUserPointer(Application).?; 133 | app.brushSize = @max(1, app.brushSize + @as(i32, @intFromFloat(height))); 134 | std.log.debug("Brush size: {}", .{app.brushSize}); 135 | } 136 | 137 | pub fn on_key(window: glfw.Window, key: glfw.Key, scancode: i32, action: glfw.Action, mods: glfw.Mods) void { 138 | _ = mods; 139 | _ = scancode; 140 | 141 | switch (action) { 142 | .release => return, 143 | .repeat => return, 144 | else => {}, 145 | } 146 | 147 | var app: *Application = window.getUserPointer(Application).?; 148 | switch (key) { 149 | .zero => app.currentMaterial = 0, 150 | .one => app.currentMaterial = 1, 151 | .two => app.currentMaterial = 2, 152 | .three => app.currentMaterial = 3, 153 | .four => app.currentMaterial = 4, 154 | .five => app.currentMaterial = 5, 155 | .six => app.currentMaterial = 6, 156 | .seven => app.currentMaterial = 7, 157 | .eight => app.currentMaterial = 8, 158 | .nine => app.currentMaterial = 9, 159 | .b => { 160 | app.brushType = switch (app.brushType) { 161 | .circle => .square, 162 | .square => .hline, 163 | .hline => .circle, 164 | }; 165 | std.log.debug("Brush type is now {}", .{app.brushType}); 166 | }, 167 | .space => { 168 | app.simState = switch (app.simState) { 169 | .running => .paused, 170 | .paused => .running, 171 | else => .paused, 172 | }; 173 | std.log.debug("Simulation state is now {}", .{app.simState}); 174 | }, 175 | .p => { 176 | app.simState = .step; 177 | std.log.debug("Stepping simulation", .{}); 178 | }, 179 | .w => { 180 | app.do_worldgen(); 181 | std.log.debug("Generating world", .{}); 182 | }, 183 | .r => { 184 | if (is_debug_build) 185 | app.reload_sim_shader(); 186 | }, 187 | else => {}, 188 | } 189 | } 190 | 191 | pub fn update(app: *@This()) void { 192 | // updating the globals 193 | const size = app.window.getSize(); 194 | const pos = app.window.getCursorPos(); 195 | const inputState: i32 = @as(i32, @intFromBool(app.window.getMouseButton(glfw.MouseButton.right) == glfw.Action.press)) << 2 | @as(i32, @intFromBool(app.window.getMouseButton(glfw.MouseButton.middle) == glfw.Action.press)) << 1 | @as(i32, @intFromBool(app.window.getMouseButton(glfw.MouseButton.left) == glfw.Action.press)); 196 | app.globals.update(ApplicationState{ 197 | .brushPos = [2]i32{ @intFromFloat(pos.xpos), @as(i32, @intCast(size.height)) - @as(i32, @intFromFloat(pos.ypos)) }, 198 | .brushSize = @floatFromInt(app.brushSize), 199 | .brushType = app.brushType, 200 | .material = app.currentMaterial, 201 | .inputState = inputState, 202 | .time = @floatCast(glfw.getTime()), 203 | .simRunning = @intFromBool(app.simState != .paused), 204 | }); 205 | } 206 | 207 | // Draw the world to the render texture and then blit it to the screen. 208 | pub fn draw(app: *@This()) void { 209 | const size = app.window.getSize(); 210 | const workgroupSize = app.drawPipeline.getWorkgroupSize(); 211 | const workgroupCount = [_]u32{ @intFromFloat(@ceil(@as(f32, @floatFromInt(size.width)) / @as(f32, @floatFromInt(workgroupSize[0])))), @intFromFloat(@ceil(@as(f32, @floatFromInt(size.height)) / @as(f32, @floatFromInt(workgroupSize[1])))), workgroupSize[2] }; 212 | const nextFrameWorldImageIndex = 1 - app.currentWorldImageIndex; 213 | 214 | // brush pipeline 215 | // apply the brush to the current world texture 216 | app.brushPipeline.use(); 217 | app.worldTexture.bind_image_layer(0, @intCast(app.currentWorldImageIndex), graphics.gl.WRITE_ONLY); 218 | app.brushPipeline.dispatch(workgroupCount[0], workgroupCount[1], workgroupCount[2]); 219 | 220 | // sim pipeline 221 | // runs the per-pixel simulation 222 | app.simPipeline.use(); 223 | app.worldTexture.bind_image_layer(0, @intCast(app.currentWorldImageIndex), graphics.gl.WRITE_ONLY); 224 | app.worldTexture.bind_image_layer(1, @intCast(nextFrameWorldImageIndex), graphics.gl.READ_ONLY); 225 | app.simPipeline.dispatch(@intCast(size.width), @intCast(size.height), 1); 226 | 227 | // draw pipeline 228 | // draws the world to the render texture 229 | app.drawPipeline.use(); 230 | app.renderTexture.bind_image(0, graphics.gl.WRITE_ONLY); 231 | app.worldTexture.bind_image_layer(1, @intCast(app.currentWorldImageIndex), graphics.gl.READ_ONLY); 232 | app.drawPipeline.dispatch(workgroupCount[0], workgroupCount[1], workgroupCount[2]); 233 | app.renderFramebuffer.blit(0, @intCast(size.width), @intCast(size.height)); 234 | 235 | app.currentWorldImageIndex = nextFrameWorldImageIndex; 236 | } 237 | 238 | pub fn run(app: *@This()) void { 239 | app.window.setUserPointer(app); 240 | app.window.setFramebufferSizeCallback(Application.on_resize); 241 | app.window.setScrollCallback(Application.on_scroll); 242 | app.window.setKeyCallback(Application.on_key); 243 | app.window.setInputMode(glfw.Window.InputMode.cursor, glfw.Window.InputModeCursor.hidden); 244 | app.do_worldgen(); 245 | while (!app.window.shouldClose()) { 246 | glfw.pollEvents(); 247 | app.update(); 248 | app.draw(); 249 | if (app.simState == .step) 250 | app.simState = .paused; 251 | app.window.swapBuffers(); 252 | } 253 | } 254 | }; 255 | -------------------------------------------------------------------------------- /src/graphics.zig: -------------------------------------------------------------------------------- 1 | pub const gl = @import("gl45.zig"); 2 | const glfw = @import("glfw"); 3 | const std = @import("std"); 4 | 5 | fn glGetProcAddress(p: glfw.GLProc, proc: [:0]const u8) ?gl.FunctionPointer { 6 | _ = p; 7 | return glfw.getProcAddress(proc); 8 | } 9 | 10 | // read a whole file into a string. 11 | fn readToEnd(file: []const u8, alloc: std.mem.Allocator) ![:0]const u8 { 12 | const fileSt = try std.fs.cwd().openFile(file, std.fs.File.OpenFlags{ .mode = .read_only }); 13 | const sourceStat = try fileSt.stat(); 14 | const source = try std.fs.File.readToEndAllocOptions(fileSt, alloc, @as(usize, 2 * sourceStat.size), @as(usize, sourceStat.size), 1, 0); 15 | return source; 16 | } 17 | 18 | // read a whole file into a string, and replace all #include statements with the contents of the included file. 19 | pub fn readGLSLSource(filepath: []const u8, alloc: std.mem.Allocator) ![:0]const u8 { 20 | const file = try std.fs.cwd().openFile(filepath, std.fs.File.OpenFlags{ .mode = .read_only }); 21 | defer file.close(); 22 | 23 | var finalSource = std.ArrayList(u8).init(alloc); 24 | var sourceWriter = finalSource.writer(); 25 | 26 | const fileReader = file.reader(); 27 | var buffered_line: [1024]u8 = undefined; 28 | while (try fileReader.readUntilDelimiterOrEof(&buffered_line, '\r')) |line| { 29 | if (std.mem.indexOf(u8, line, "#include")) |index| { 30 | const fileName = line[index + 9 ..]; 31 | const depContents = readToEnd(fileName, alloc) catch |err| { 32 | std.log.err("Failed to read GLSL file {s}: {}", .{ fileName, err }); 33 | continue; 34 | }; 35 | try sourceWriter.writeAll(depContents); 36 | alloc.free(depContents); 37 | } else { 38 | try sourceWriter.writeAll(line); 39 | } 40 | } 41 | return try finalSource.toOwnedSliceSentinel(0); 42 | } 43 | 44 | /// Loads OpenGL functions for the given window. 45 | pub fn loadOpenGL(window: glfw.Window) !void { 46 | glfw.makeContextCurrent(window); 47 | const glproc: glfw.GLProc = undefined; 48 | try gl.load(glproc, glGetProcAddress); 49 | } 50 | 51 | // Returns a compiled shader handle of the given type. 52 | pub fn getShader(file: []const u8, shader_kind: gl.GLenum, alloc: std.mem.Allocator) !gl.GLuint { 53 | const source = try readGLSLSource(file, alloc); 54 | std.log.info("Compiling shader: {s}", .{file}); 55 | 56 | const handle = gl.createShader(shader_kind); 57 | gl.shaderSource(handle, 1, @ptrCast(&source), null); 58 | gl.compileShader(handle); 59 | var info_log: [1024]u8 = undefined; 60 | var info_log_len: gl.GLsizei = undefined; 61 | gl.getShaderInfoLog(handle, 1024, &info_log_len, &info_log); 62 | if (info_log_len != 0) { 63 | std.log.info("Error while compiling shader {s} : {s}", .{ file, info_log[0..@intCast(info_log_len)] }); 64 | return error.ShaderCompilationError; 65 | } 66 | alloc.free(source); 67 | return handle; 68 | } 69 | 70 | // Returns a linked shader program from the given shaders. 71 | pub fn getShaderProgram(shadersHandles: anytype) !gl.GLuint { 72 | const program = gl.createProgram(); 73 | inline for (shadersHandles) |handle| { 74 | gl.attachShader(program, handle); 75 | } 76 | var link_status: gl.GLint = undefined; 77 | var info_log: [1024]u8 = undefined; 78 | var info_log_len: gl.GLsizei = undefined; 79 | gl.linkProgram(program); 80 | gl.getProgramiv(program, gl.LINK_STATUS, &link_status); 81 | gl.getProgramInfoLog(program, 1024, &info_log_len, &info_log); 82 | if (link_status == gl.FALSE) { 83 | std.log.info("Failed to link shader program: {s}", .{info_log[0..@intCast(info_log_len)]}); 84 | gl.deleteProgram(program); 85 | return error.ProgramLinkError; 86 | } else { 87 | std.log.info("Shader program linked OK", .{}); 88 | return program; 89 | } 90 | } 91 | 92 | /// Represents a compute pipeline. 93 | pub const ComputePipeline = struct { 94 | program: gl.GLuint, 95 | 96 | pub fn init(file: []const u8, alloc: std.mem.Allocator) !@This() { 97 | const shader = try getShader(file, gl.COMPUTE_SHADER, alloc); 98 | const compute_prog = try getShaderProgram(.{shader}); 99 | 100 | return .{ .program = compute_prog }; 101 | } 102 | 103 | /// Dispatches the compute shader for execution with the given dimensions. 104 | pub fn dispatch(this: *@This(), x: gl.GLuint, y: gl.GLuint, z: gl.GLuint) void { 105 | _ = this; 106 | gl.dispatchCompute(x, y, z); 107 | gl.memoryBarrier(gl.SHADER_IMAGE_ACCESS_BARRIER_BIT); 108 | } 109 | 110 | /// Binds this program for use. 111 | /// You should call and set uniforms before calling this. 112 | pub fn use(this: *@This()) void { 113 | gl.useProgram(this.program); 114 | } 115 | 116 | pub fn deinit(this: *@This()) void { 117 | gl.deleteProgram(this.program); 118 | } 119 | 120 | pub fn getWorkgroupSize(this: *@This()) [3]u32 { 121 | var workgroup_size: [3]gl.GLint = undefined; 122 | gl.getProgramiv(this.program, gl.COMPUTE_WORK_GROUP_SIZE, &workgroup_size); 123 | return [3]u32{ @intCast(workgroup_size[0]), @intCast(workgroup_size[1]), @intCast(workgroup_size[2]) }; 124 | } 125 | }; 126 | 127 | /// A texture that can be used as a storage image in compute shaders. 128 | pub const Texture = struct { 129 | texture: gl.GLuint, 130 | w: u32, 131 | h: u32, 132 | format: gl.GLenum, 133 | num_layers: u32, 134 | 135 | pub fn init(width: u32, height: u32, format: gl.GLenum, num_layers: u32) @This() { 136 | var texture: gl.GLuint = undefined; 137 | if (num_layers > 1) { 138 | gl.createTextures(gl.TEXTURE_3D, 1, &texture); 139 | gl.textureStorage3D(texture, 1, format, @intCast(width), @intCast(height), @intCast(num_layers)); 140 | } else { 141 | gl.createTextures(gl.TEXTURE_2D, 1, &texture); 142 | gl.textureStorage2D(texture, 1, format, @intCast(width), @intCast(height)); 143 | } 144 | 145 | return .{ .texture = texture, .w = width, .h = height, .format = format, .num_layers = num_layers }; 146 | } 147 | 148 | /// Bind the texture as an to the given binding index for compute. 149 | pub fn bind_image(this: *@This(), bindingIndex: gl.GLuint, usage: gl.GLenum) void { 150 | gl.bindImageTexture(bindingIndex, this.texture, 0, gl.FALSE, 0, usage, this.format); 151 | } 152 | 153 | /// Same as bind_image, but for layered textures. 154 | pub fn bind_image_layer(this: *@This(), bindingIndex: gl.GLuint, layer: gl.GLint, usage: gl.GLenum) void { 155 | gl.bindImageTexture(bindingIndex, this.texture, 0, gl.FALSE, layer, usage, this.format); 156 | } 157 | 158 | /// Clear the texture to the given color. 159 | pub fn clear(this: *@This(), data: []const u8) void { 160 | gl.clearTexImage(this.texture, 0, this.format, gl.UNSIGNED_BYTE, @ptrCast(data)); 161 | } 162 | 163 | pub fn set_data(this: *@This(), data_format: gl.GLenum, data: []const u8) void { 164 | gl.textureSubImage2D(this.texture, 0, 0, 0, @intCast(this.w), @intCast(this.h), this.format, data_format, data); 165 | } 166 | 167 | pub fn deinit(this: *@This()) void { 168 | gl.deleteTextures(1, &this.texture); 169 | } 170 | }; 171 | 172 | /// A framebuffer that can be used as a render target. 173 | pub const Framebuffer = struct { 174 | framebuffer: gl.GLuint, 175 | 176 | pub fn init(storage: *const Texture) @This() { 177 | var framebuffer: gl.GLuint = undefined; 178 | gl.createFramebuffers(1, &framebuffer); 179 | gl.namedFramebufferTexture(framebuffer, gl.COLOR_ATTACHMENT0, storage.*.texture, 0); 180 | return .{ .framebuffer = framebuffer }; 181 | } 182 | 183 | pub fn deinit(this: *@This()) void { 184 | gl.deleteFramebuffers(1, &this.framebuffer); 185 | } 186 | 187 | pub fn blit(this: *@This(), dest: gl.GLuint, width: gl.GLsizei, height: gl.GLsizei) void { 188 | gl.blitNamedFramebuffer(this.framebuffer, dest, 0, 0, width, height, 0, 0, width, height, gl.COLOR_BUFFER_BIT, gl.NEAREST); 189 | } 190 | }; 191 | 192 | /// A bindable uniform buffer object. 193 | /// Provides a type-safe interface to a uniform buffer of underlying type `uniformStructType`. 194 | pub fn UniformBuffer(comptime uniformStructType: type) type { 195 | return struct { 196 | buffer: gl.GLuint, 197 | 198 | pub fn init() @This() { 199 | var buffer: gl.GLuint = undefined; 200 | gl.createBuffers(1, &buffer); 201 | return .{ .buffer = buffer }; 202 | } 203 | 204 | /// Bind the uniform buffer to the given binding index. 205 | pub fn bind(this: *@This(), program: gl.GLuint, bindingIndex: u32, name: [:0]const u8) void { 206 | const blockIndex = gl.getUniformBlockIndex(program, name); 207 | gl.uniformBlockBinding(program, blockIndex, @intCast(bindingIndex)); 208 | gl.bindBufferBase(gl.UNIFORM_BUFFER, bindingIndex, this.buffer); 209 | } 210 | 211 | // Update the buffer with the given data. 212 | // This should be called once per frame. 213 | pub fn update(this: *@This(), data: uniformStructType) void { 214 | gl.namedBufferData(this.buffer, @intCast(@sizeOf(uniformStructType)), &data, gl.STREAM_READ); 215 | } 216 | 217 | pub fn deinit(this: *@This()) void { 218 | gl.deleteBuffers(1, &this.buffer); 219 | } 220 | }; 221 | } 222 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Application = @import("app.zig").Application; 3 | const glfw = @import("glfw"); 4 | 5 | pub fn main() !void { 6 | _ = glfw.init(.{}); 7 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 8 | const alloc = gpa.allocator(); 9 | var application = try Application.init(alloc); 10 | application.run(); 11 | glfw.terminate(); 12 | _ = gpa.deinit(); 13 | } 14 | -------------------------------------------------------------------------------- /src/material.zig: -------------------------------------------------------------------------------- 1 | pub const MaterialInfo = extern struct { 2 | baseColor: [4]i32, 3 | density: f32 align(16), 4 | }; 5 | 6 | // zig fmt: off 7 | pub const MATERIAL_LIST = [_]MaterialInfo{ 8 | MaterialInfo{ // void 9 | .baseColor = [_]i32{ 80, 80, 80, 255 }, 10 | .density = 0.0, 11 | }, 12 | MaterialInfo{ // sand 13 | .baseColor = [_]i32{ 194, 178, 128, 255 }, 14 | .density = 1.0, 15 | }, 16 | MaterialInfo { // wall 17 | .baseColor = [_]i32{ 120, 120, 120, 120 }, 18 | .density = 1.0, 19 | }, 20 | MaterialInfo { // dirt 21 | .baseColor = [_]i32{ 124, 92, 60, 255 }, 22 | .density = 1.0, 23 | }, 24 | MaterialInfo { //water 25 | .baseColor = [_]i32{35, 137, 218, 255 }, 26 | .density = 1.0, 27 | }, 28 | MaterialInfo { // stone 29 | .baseColor = [_]i32{174, 162 ,149, 255 }, 30 | .density = 8.0, 31 | }, 32 | MaterialInfo { // acid 33 | .baseColor = [_]i32{ 0, 255, 60, 255 }, 34 | .density = 1.0, 35 | }, 36 | MaterialInfo { // moss 37 | .baseColor = [_]i32{ 0, 100, 0, 255 }, 38 | .density = 1.0, 39 | }, 40 | MaterialInfo { // lava 41 | .baseColor = [_]i32{255, 128, 0, 255}, 42 | .density = 1.0, 43 | } 44 | }; 45 | --------------------------------------------------------------------------------