1 //------------------------------------------------------------------------------
2 //  mrt.d
3 //
4 //  Rendering with multiple-rendertargets, and reallocate render targets
5 //  on window resize events.
6 //
7 //  NOTE: the rotation direction will appear different on the different
8 //  backend 3D APIs. This is because of the different image origin conventions
9 //  in GL vs D3D vs Metal. We don't care about those differences in this sample
10 //  (using the sokol shader compiler allows to easily 'normalize' those differences.
11 //------------------------------------------------------------------------------
12 module examples.mrt;
13 
14 private:
15 
16 import sg = sokol.gfx;
17 import app = sokol.app;
18 import log = sokol.log;
19 import handmade.math : Mat4, Vec3, Vec2, sin, cos;
20 import sglue = sokol.glue;
21 import shd = shaders.mrt;
22 
23 extern (C):
24 @safe:
25 
26 enum OFFSCREEN_SAMPLE_COUNT = 1;
27 enum NUM_MRTS = 3;
28 
29 struct Images
30 {
31     sg.Image[NUM_MRTS] color = [];
32     sg.Image[NUM_MRTS] resolve = [];
33     sg.Image depth = {};
34 }
35 
36 struct Offscreen
37 {
38     sg.Pass pass = {
39         action: {
40             colors: [
41                 { load_action: sg.LoadAction.Clear, clear_value: { r: 0.25, g: 0, b: 0, a: 1 } },
42                 { load_action: sg.LoadAction.Clear, clear_value: { r: 0, g: 0.25, b: 0, a: 1 } },
43                 { load_action: sg.LoadAction.Clear, clear_value: { r: 0, g: 0, b: 0.25, a: 1 } },
44             ],
45         },
46     };
47     sg.Pipeline pip;
48     sg.Bindings bind;
49 }
50 
51 struct Display
52 {
53     sg.Pipeline pip;
54     sg.Bindings bind;
55     sg.PassAction pass_action = {
56         colors: [{load_action: sg.LoadAction.Dontcare}],
57         depth: {load_action: sg.LoadAction.Dontcare},
58         stencil: {load_action: sg.LoadAction.Dontcare},
59     };
60 }
61 
62 struct Dbg
63 {
64     sg.Pipeline pip;
65     sg.Bindings bind;
66 }
67 
68 struct State
69 {
70     Images images;
71     Offscreen offscreen;
72     Display display;
73     Dbg dbg;
74     float rx = 0;
75     float ry = 0;
76     Mat4 view;
77 }
78 
79 static State state;
80 
81 void init()
82 {
83     sg.Desc gfxd = {
84         environment: sglue.environment,
85         logger: {func: &log.slog_func}
86     };
87     sg.setup(gfxd);
88 
89     // setup the offscreen render pass and render target images,
90     // this will also be called when the window resizes
91     recreateOffscreenAttachments(app.width(), app.height());
92 
93     // create vertex buffer for a cube
94     float[96] vertices = [
95         // positions        brightness
96         -1.0, -1.0, -1.0, 1.0,
97         1.0, -1.0, -1.0, 1.0,
98         1.0, 1.0, -1.0, 1.0,
99         -1.0, 1.0, -1.0, 1.0,
100 
101         -1.0, -1.0, 1.0, 0.8,
102         1.0, -1.0, 1.0, 0.8,
103         1.0, 1.0, 1.0, 0.8,
104         -1.0, 1.0, 1.0, 0.8,
105 
106         -1.0, -1.0, -1.0, 0.6,
107         -1.0, 1.0, -1.0, 0.6,
108         -1.0, 1.0, 1.0, 0.6,
109         -1.0, -1.0, 1.0, 0.6,
110 
111         1.0, -1.0, -1.0, 0.0,
112         1.0, 1.0, -1.0, 0.0,
113         1.0, 1.0, 1.0, 0.0,
114         1.0, -1.0, 1.0, 0.0,
115 
116         -1.0, -1.0, -1.0, 0.5,
117         -1.0, -1.0, 1.0, 0.5,
118         1.0, -1.0, 1.0, 0.5,
119         1.0, -1.0, -1.0, 0.5,
120 
121         -1.0, 1.0, -1.0, 0.7,
122         -1.0, 1.0, 1.0, 0.7,
123         1.0, 1.0, 1.0, 0.7,
124         1.0, 1.0, -1.0, 0.7,
125     ];
126     sg.BufferDesc cube_vbuf_desc = {
127         data: {ptr: vertices.ptr,
128         size: vertices.sizeof,},
129     };
130     state.offscreen.bind.vertex_buffers[0] = sg.makeBuffer(cube_vbuf_desc);
131 
132     // index buffer for a cube
133     ushort[36] indices = [
134         0, 1, 2, 0, 2, 3,
135         6, 5, 4, 7, 6, 4,
136         8, 9, 10, 8, 10, 11,
137         14, 13, 12, 15, 14, 12,
138         16, 17, 18, 16, 18, 19,
139         22, 21, 20, 23, 22, 20,
140     ];
141     sg.BufferDesc cube_ibuf_desc = {
142         usage: { index_buffer: true },
143         data: {ptr: indices.ptr, size: indices.sizeof},
144     };
145     state.offscreen.bind.index_buffer = sg.makeBuffer(cube_ibuf_desc);
146 
147     // shader and pipeline state object for rendering cube into MRT render targets
148     sg.PipelineDesc offscreen_pip_desc = {
149         layout: {
150             attrs: [
151                 shd.ATTR_OFFSCREEN_POS: {format: sg.VertexFormat.Float3},
152                 shd.ATTR_OFFSCREEN_BRIGHT0: {format: sg.VertexFormat.Float},
153             ]
154         },
155         shader: sg.makeShader(shd.offscreenShaderDesc(sg.queryBackend())),
156         index_type: sg.IndexType.Uint16,
157         cull_mode: sg.CullMode.Back,
158         sample_count: OFFSCREEN_SAMPLE_COUNT,
159         depth: {
160             pixel_format: sg.PixelFormat.Depth,
161             compare: sg.CompareFunc.Less_equal,
162             write_enabled: true,
163         },
164         color_count: 3,
165     };
166     state.offscreen.pip = sg.makePipeline(offscreen_pip_desc);
167 
168     // a vertex buffer to render a fullscreen quad
169     float[8] quad_vertices = [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0];
170     sg.BufferDesc quad_vbuf_desc = {
171         data: {ptr: quad_vertices.ptr, size: quad_vertices.sizeof},
172     };
173     const quad_vbuf = sg.makeBuffer(quad_vbuf_desc);
174     state.display.bind.vertex_buffers[0] = quad_vbuf;
175     state.dbg.bind.vertex_buffers[0] = quad_vbuf;
176 
177     // shader and pipeline object to render a fullscreen quad which composes
178     // the 3 offscreen render targets into the default framebuffer
179     sg.PipelineDesc fsq_pip_desc = {
180         layout: {attrs: [
181             shd.ATTR_FSQ_POS: {format: sg.VertexFormat.Float2},
182         ]},
183         shader: sg.makeShader(shd.fsqShaderDesc(sg.queryBackend())),
184         primitive_type: sg.PrimitiveType.Triangle_strip,
185     };
186     state.display.pip = sg.makePipeline(fsq_pip_desc);
187 
188     // a sampler to sample the offscreen render targets as texture
189     sg.SamplerDesc smp_desc = {
190         min_filter: sg.Filter.Linear,
191         mag_filter: sg.Filter.Linear,
192         wrap_u: sg.Wrap.Clamp_to_edge,
193         wrap_v: sg.Wrap.Clamp_to_edge,
194     };
195     const smp = sg.makeSampler(smp_desc);
196     state.display.bind.samplers[shd.SMP_SMP] = smp;
197     state.dbg.bind.samplers[shd.SMP_SMP] = smp;
198 
199     // shader, pipeline and resource bindings to render debug visualization quads
200     sg.PipelineDesc dbg_pip_desc = {
201         layout: {attrs: [
202             shd.ATTR_DBG_POS: {format: sg.VertexFormat.Float2},
203         ]},
204         shader: sg.makeShader(shd.dbgShaderDesc(sg.queryBackend())),
205         primitive_type: sg.PrimitiveType.Triangle_strip,
206     };
207     state.dbg.pip = sg.makePipeline(dbg_pip_desc);
208 }
209 
210 void frame()
211 {
212     immutable(float) dt = (app.frameDuration() * 60.0);
213     state.rx += 1.0 * dt;
214     state.ry += 2.0 * dt;
215 
216     // compute shader uniform data
217     shd.OffscreenParams offscreen_params = {
218         mvp: computeMvp(state.rx, state.ry),
219     };
220     shd.FsqParams fsq_params = {
221         offset: Vec2(sin(state.rx * 0.01) * 0.1, cos(state.ry * 0.01) * 0.1),
222     };
223 
224     // render cube into MRT offscreen render targets
225     sg.beginPass(state.offscreen.pass);
226     sg.applyPipeline(state.offscreen.pip);
227     sg.applyBindings(state.offscreen.bind);
228     sg.Range offs_rg = {ptr: &offscreen_params, offscreen_params.sizeof};
229     sg.applyUniforms(shd.UB_OFFSCREEN_PARAMS, offs_rg);
230     sg.draw(0, 36, 1);
231     sg.endPass();
232 
233     // render fullscreen quad with the composed offscreen-render images,
234     // 3 a small debug view quads at the bottom of the screen
235     sg.Pass pass_swap = {
236         action: state.display.pass_action, swapchain: sglue.swapchain
237     };
238     sg.beginPass(pass_swap);
239     sg.applyPipeline(state.display.pip);
240     sg.applyBindings(state.display.bind);
241     sg.Range fsq_rg = {ptr: &fsq_params, size: fsq_params.sizeof};
242     sg.applyUniforms(shd.UB_FSQ_PARAMS, fsq_rg);
243     sg.draw(0, 4, 1);
244     sg.applyPipeline(state.dbg.pip);
245     foreach (i; [0, 1, 2])
246     {
247         sg.applyViewport(i * 100, 0, 100, 100, false);
248         state.dbg.bind.views[shd.VIEW_TEX] = state.display.bind.views[i];
249         sg.applyBindings(state.dbg.bind);
250         sg.draw(0, 4, 1);
251     }
252     sg.endPass();
253     sg.commit();
254 }
255 
256 void event(const app.Event* ev)
257 {
258     if (ev.type == app.EventType.Resized)
259     {
260         recreateOffscreenAttachments(ev.framebuffer_width, ev.framebuffer_height);
261     }
262 }
263 
264 void cleanup()
265 {
266     sg.shutdown();
267 }
268 
269 void main()
270 {
271     app.Desc runner = {
272         window_title: "mrt.d",
273         init_cb: &init,
274         frame_cb: &frame,
275         cleanup_cb: &cleanup,
276         event_cb: &event,
277         width: 800,
278         height: 600,
279         sample_count: 1,
280         icon: {sokol_default: true},
281         logger: {func: &log.func}
282     };
283     app.run(runner);
284 }
285 
286 void recreateOffscreenAttachments(int width, int height)
287 {
288     // destroy and re-create color-, resolve- and depth-stencil-attachment images and views
289     for (int i = 0; i < NUM_MRTS; i++) {
290         // color attachment images and views
291         sg.destroyImage(state.images.color[i]);
292         sg.ImageDesc color_image_desc = {
293             usage: { color_attachment: true },
294             width: width,
295             height: height,
296             sample_count: OFFSCREEN_SAMPLE_COUNT,
297         };
298         state.images.color[i] = sg.makeImage(color_image_desc);
299         sg.destroyView(state.offscreen.pass.attachments.colors[i]);
300         sg.ViewDesc color_attview_desc = {
301             color_attachment: { image: state.images.color[i] },
302         };
303         state.offscreen.pass.attachments.colors[i] = sg.makeView(color_attview_desc);
304 
305         // resolve attachment images and views
306         sg.destroyImage(state.images.resolve[i]);
307         sg.ImageDesc resolve_image_desc = {
308             usage: { resolve_attachment: true },
309             width: width,
310             height: height,
311             sample_count: 1,
312         };
313         state.images.resolve[i] = sg.makeImage(resolve_image_desc);
314         sg.destroyView(state.offscreen.pass.attachments.resolves[i]);
315         sg.ViewDesc resolve_attview_desc = {
316             resolve_attachment: { image: state.images.resolve[i] },
317         };
318         state.offscreen.pass.attachments.resolves[i] = sg.makeView(resolve_attview_desc);
319 
320         // the resolve images are also sampled as textures, so need texture views
321         sg.destroyView(state.display.bind.views[i]);
322         sg.ViewDesc resolve_texview_desc = {
323             texture: { image: state.images.resolve[i] },
324         };
325         state.display.bind.views[i] = sg.makeView(resolve_texview_desc);
326     }
327 
328     // depth-stencil-attachment image and view
329     sg.destroyImage(state.images.depth);
330     sg.ImageDesc depth_image_desc = {
331         usage: { depth_stencil_attachment: true },
332         width: width,
333         height: height,
334         sample_count: OFFSCREEN_SAMPLE_COUNT,
335         pixel_format: sg.PixelFormat.Depth,
336     };
337     state.images.depth = sg.makeImage(depth_image_desc);
338     sg.destroyView(state.offscreen.pass.attachments.depth_stencil);
339     sg.ViewDesc depth_attview_desc = {
340         depth_stencil_attachment: { image: state.images.depth },
341     };
342     state.offscreen.pass.attachments.depth_stencil = sg.makeView(depth_attview_desc);
343 }
344 
345 Mat4 computeMvp(float rx, float ry)
346 {
347     immutable proj = Mat4.perspective(60.0, app.widthf() / app.heightf(), 0.01, 10.0);
348     immutable view = Mat4.lookAt(Vec3(0.0, 1.5, 6.0), Vec3.zero, Vec3.up);
349     immutable rxm = Mat4.rotate(rx, Vec3(1.0, 0.0, 0.0));
350     immutable rym = Mat4.rotate(ry, Vec3(0.0, 1.0, 0.0));
351     immutable model = Mat4.mul(rxm, rym);
352     immutable view_proj = Mat4.mul(proj, view);
353 
354     return Mat4.mul(view_proj, model);
355 }
356 
357 version (WebAssembly)
358 {
359     debug
360     {
361         import emscripten.assertd;
362     }
363 }