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 }