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 }