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 }