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