├── .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 | 
19 | 
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------