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