1 //------------------------------------------------------------------------------ 2 // instancingcompute.d 3 // 4 // Like instancing.d, but use a compute shader to update particles. 5 //------------------------------------------------------------------------------ 6 module examples.instancingcompute; 7 8 private: 9 10 import sg = sokol.gfx; 11 import sapp = sokol.app; 12 import slog = sokol.log; 13 import handmade.math: Mat4, Vec3; 14 import sglue = sokol.glue; 15 import shd = shaders.instancingcompute; 16 17 extern (C): 18 @safe nothrow @nogc: 19 20 enum max_particles = 512 * 1024; 21 enum num_particles_emitted_per_frame = 10; 22 23 struct ComputeState { 24 sg.Pipeline pip = {}; 25 sg.Bindings bind = {}; 26 } 27 28 struct DisplayState { 29 sg.Pipeline pip = {}; 30 sg.Bindings bind = {}; 31 sg.PassAction pass_action = { 32 colors: [ 33 { 34 load_action: sg.LoadAction.Clear, 35 clear_value: {r: 0.0, g: 0.1, b: 0.2, a: 1 }, 36 } 37 ] 38 }; 39 } 40 41 struct State { 42 int num_particles = 0; 43 float ry = 0; 44 ComputeState compute; 45 DisplayState display; 46 47 Mat4 view() @nogc nothrow const { 48 return Mat4.lookAt(Vec3(0.0, 1.5, 12.0), Vec3.zero(), Vec3.up()); 49 } 50 } 51 52 static State state; 53 54 void init() { 55 sg.Desc gfxd = { 56 environment: sglue.environment, 57 logger: { func: &slog.func }, 58 }; 59 sg.setup(gfxd); 60 61 // if compute shaders not supported, clear to red color and early out 62 if (!sg.queryFeatures().compute) { 63 state.display.pass_action.colors[0].clear_value.r = 1.0; 64 state.display.pass_action.colors[0].clear_value.g = 0.0; 65 state.display.pass_action.colors[0].clear_value.b = 0.0; 66 return; 67 } 68 69 // a zero-initialized storage buffer for the particle state 70 sg.BufferDesc sbufd = { 71 usage: { storage_buffer: true }, 72 size: shd.Particle.sizeof * max_particles, 73 label: "particle-buffer", 74 }; 75 sg.Buffer sbuf = sg.makeBuffer(sbufd); 76 sg.ViewDesc sbufviewd = { 77 storage_buffer: { buffer: sbuf }, 78 }; 79 sg.View sbuf_view = sg.makeView(sbufviewd); 80 state.compute.bind.views[shd.VIEW_CS_SSBO] = sbuf_view; 81 state.display.bind.views[shd.VIEW_VS_SSBO] = sbuf_view; 82 83 // a compute shader and pipeline object for updating the particle state 84 sg.PipelineDesc upipd = { 85 compute: true, 86 shader: sg.makeShader(shd.updateShaderDesc(sg.queryBackend())), 87 label: "update-pipeline", 88 }; 89 state.compute.pip = sg.makePipeline(upipd); 90 91 // vertex and index buffer for the particle geometry 92 immutable float r = 0.05; 93 float[42] vertices = [ 94 0.0, -r, 0.0, 1.0, 0.0, 0.0, 1.0, 95 r, 0.0, r, 0.0, 1.0, 0.0, 1.0, 96 r, 0.0, -r, 0.0, 0.0, 1.0, 1.0, 97 -r, 0.0, -r, 1.0, 1.0, 0.0, 1.0, 98 -r, 0.0, r, 0.0, 1.0, 1.0, 1.0, 99 0.0, r, 0.0, 1.0, 0.0, 1.0, 1.0, 100 ]; 101 ushort[24] indices = [ 102 2, 1, 0, 3, 2, 0, 103 4, 3, 0, 1, 4, 0, 104 5, 1, 2, 5, 2, 3, 105 5, 3, 4, 5, 4, 1, 106 ]; 107 sg.BufferDesc vbufd = { 108 data: { ptr: vertices.ptr, size: vertices.sizeof }, 109 label: "geometry-vbuf", 110 }; 111 sg.BufferDesc ibufd = { 112 usage: { index_buffer: true }, 113 data: { ptr: indices.ptr, size: indices.sizeof }, 114 label: "geometry-ibuf", 115 }; 116 state.display.bind.vertex_buffers[0] = sg.makeBuffer(vbufd); 117 state.display.bind.index_buffer = sg.makeBuffer(ibufd); 118 119 // shader and pipeline for rendering the particles, this uses 120 // the compute-updated storage buffer to provide the particle positions 121 sg.PipelineDesc rpipd = { 122 shader: sg.makeShader(shd.displayShaderDesc(sg.queryBackend())), 123 layout: { 124 attrs: [ 125 shd.ATTR_DISPLAY_POS: { format: sg.VertexFormat.Float3 }, 126 shd.ATTR_DISPLAY_COLOR0: { format: sg.VertexFormat.Float4 }, 127 ], 128 }, 129 index_type: sg.IndexType.Uint16, 130 cull_mode: sg.CullMode.Back, 131 depth: { 132 compare: sg.CompareFunc.Less_equal, 133 write_enabled: true, 134 }, 135 label: "render-pipeline", 136 }; 137 state.display.pip = sg.makePipeline(rpipd); 138 139 // one-time init of particle velocities via a compute shader 140 sg.PipelineDesc ipipd = { 141 compute: true, 142 shader: sg.makeShader(shd.initShaderDesc(sg.queryBackend())), 143 }; 144 sg.Pipeline ipip = sg.makePipeline(ipipd); 145 sg.Pass pass = { compute: true }; 146 sg.beginPass(pass); 147 sg.applyPipeline(ipip); 148 sg.applyBindings(state.compute.bind); 149 sg.dispatch(max_particles / 64, 1, 1); 150 sg.endPass(); 151 } 152 153 void frame() { 154 if (!sg.queryFeatures().compute) { 155 drawFallback(); 156 return; 157 } 158 159 state.num_particles += num_particles_emitted_per_frame; 160 if (state.num_particles > max_particles) { 161 state.num_particles = max_particles; 162 } 163 float dt = sapp.frameDuration(); 164 165 // compute pass to update particle positions 166 shd.CsParams cs_params = { 167 dt: dt, 168 num_particles: state.num_particles, 169 }; 170 sg.Range cs_params_range = { ptr: &cs_params, size: cs_params.sizeof }; 171 sg.Pass cpass = { compute: true, label: "compute-pass" }; 172 sg.beginPass(cpass); 173 sg.applyPipeline(state.compute.pip); 174 sg.applyBindings(state.compute.bind); 175 sg.applyUniforms(shd.UB_CS_PARAMS, cs_params_range); 176 sg.dispatch((state.num_particles + 63) / 64, 1, 1); 177 sg.endPass(); 178 179 // render pass to render the particles via instancing, with the 180 // instance positions coming from the storage buffer 181 state.ry += 60.0 * dt; 182 shd.VsParams vs_params = computeVsParams(1.0, state.ry); 183 sg.Range vs_params_range = { ptr: &vs_params, size: vs_params.sizeof }; 184 sg.Pass rpass = { 185 action: state.display.pass_action, 186 swapchain: sglue.swapchain(), 187 label: "render-pass", 188 }; 189 sg.beginPass(rpass); 190 sg.applyPipeline(state.display.pip); 191 sg.applyBindings(state.display.bind); 192 sg.applyUniforms(shd.UB_VS_PARAMS, vs_params_range); 193 sg.draw(0, 24, state.num_particles); 194 sg.endPass(); 195 sg.commit(); 196 } 197 198 void cleanup() { 199 sg.shutdown(); 200 } 201 202 void drawFallback() { 203 sg.Pass rpass = { 204 action: state.display.pass_action, 205 swapchain: sglue.swapchain(), 206 label: "render-pass", 207 }; 208 sg.beginPass(rpass); 209 sg.endPass(); 210 sg.commit(); 211 } 212 213 shd.VsParams computeVsParams(float rx, float ry) @nogc nothrow 214 { 215 immutable proj = Mat4.perspective(60.0, sapp.widthf() / sapp.heightf(), 0.01, 50.0); 216 immutable rxm = Mat4.rotate(rx, Vec3(1.0, 0.0, 0.0)); 217 immutable rym = Mat4.rotate(ry, Vec3(0.0, 1.0, 0.0)); 218 immutable model = Mat4.mul(rxm, rym); 219 immutable view_proj = Mat4.mul(proj, state.view()); 220 shd.VsParams mvp = { mvp: Mat4.mul(view_proj, model) }; 221 return mvp; 222 } 223 224 void main() { 225 sapp.Desc adesc = { 226 init_cb: &init, 227 frame_cb: &frame, 228 cleanup_cb: &cleanup, 229 width: 800, 230 height: 600, 231 sample_count: 4, 232 window_title: "instancingcompute.d", 233 icon: { sokol_default: true }, 234 logger: { func: &slog.func }, 235 }; 236 sapp.run(adesc); 237 } 238 239 version (WebAssembly) 240 { 241 debug 242 { 243 import emscripten.assertd; 244 } 245 }