1 /** 2 The widget you must inherit from for a PBR background UI (though it isn't mandatory). 3 4 Copyright: Copyright Auburn Sounds 2015-2017. 5 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 Authors: Guillaume Piolat 7 */ 8 module dplug.pbrwidgets.pbrbackgroundgui; 9 10 import dplug.math.box; 11 import dplug.core.nogc; 12 import dplug.core.file; 13 import dplug.core.thread; 14 15 // Note: this dependency exist because Key is defined in dplug:window 16 import dplug.window.window; 17 18 import dplug.gui.graphics; 19 import dplug.gui.element; 20 import dplug.gui.compositor; 21 import dplug.gui.legacypbr; 22 public import dplug.gui.sizeconstraints; 23 24 import gamut; 25 26 27 // `decompressImagesLazily` cause JPEG and PNG to be decompressed on the fly on resize, instead 28 // of ahead of time and staying in memory. 29 // This wins 17mb of RAM on Panagement. 30 // However, this also disable live reload of images for UI development. Hence, it is disabled for debug builds, in order 31 // to reload background with the RETURN key. 32 debug {} 33 else 34 { 35 version = decompressImagesLazily; 36 } 37 38 /// PBRBackgroundGUI provides a PBR background loaded from PNG or JPEG images. 39 /// It's very practical while in development because it let's you reload the six 40 /// images used with the press of ENTER. 41 /// The path of each of these images (given as a template parameter) must be 42 /// in your "stringImportPaths" settings. 43 class PBRBackgroundGUI(string baseColorPath, 44 string emissivePath, 45 string materialPath, 46 string depthPath, 47 string skyboxPath, 48 string absoluteGfxDirectory // for UI development only 49 ) : GUIGraphics 50 { 51 public: 52 nothrow: 53 @nogc: 54 55 this(int width, int height) 56 { 57 this(makeSizeConstraintsFixed(width, height)); 58 } 59 60 this(SizeConstraints sizeConstraints) 61 { 62 super(sizeConstraints, flagPBR | flagAnimated | flagDrawAlonePBR); 63 64 _diffuseResized = mallocNew!(OwnedImage!RGBA); 65 _materialResized = mallocNew!(OwnedImage!RGBA); 66 _depthResized = mallocNew!(OwnedImage!L16); 67 68 version(decompressImagesLazily) 69 {} 70 else 71 { 72 loadBackgroundImagesFromStaticData(null); 73 } 74 75 auto skyboxData = cast(ubyte[])(import(skyboxPath)); 76 loadSkybox(skyboxData); 77 } 78 79 ~this() 80 { 81 freeBackgroundImages(); 82 _diffuseResized.destroyFree(); 83 _materialResized.destroyFree(); 84 _depthResized.destroyFree(); 85 } 86 87 // Development purposes. 88 // In debug mode, pressing ENTER reload the backgrounds 89 debug 90 { 91 override bool onKeyDown(Key key) 92 { 93 if (super.onKeyDown(key)) 94 return true; 95 96 version(decompressImagesLazily) 97 { 98 } 99 else 100 { 101 if (key == Key.enter) 102 { 103 reloadImagesAtRuntime(); 104 return true; 105 } 106 } 107 108 return false; 109 } 110 } 111 112 override void onDrawPBR(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects) 113 { 114 // Resize resource to match _position 115 { 116 int W = position.width; 117 int H = position.height; 118 119 if (_forceResizeUpdate || _diffuseResized.w != W || _diffuseResized.h != H) 120 { 121 // Decompress images lazily for this size 122 123 version (decompressImagesLazily) 124 { 125 assert(_diffuse is null); 126 127 // No thread pool, load images one by one. 128 loadBackgroundImagesFromStaticData(context.globalThreadPool); 129 } 130 131 _diffuseResized.size(W, H); 132 _materialResized.size(W, H); 133 _depthResized.size(W, H); 134 135 // Potentially resize all 3 backgrounds in parallel 136 void resizeOneImage(int i, int threadIndex) nothrow @nogc 137 { 138 ImageResizer resizer; 139 if (i == 0) 140 { 141 version(Dplug_ProfileUI) context.profiler.begin("resize Diffuse background"); 142 resizer.resizeImageDiffuse(_diffuse.toRef, _diffuseResized.toRef); 143 version(Dplug_ProfileUI) context.profiler.end; 144 } 145 if (i == 1) 146 { 147 version(Dplug_ProfileUI) context.profiler.begin("resize Material background"); 148 resizer.resizeImageMaterial(_material.toRef, _materialResized.toRef); 149 version(Dplug_ProfileUI) context.profiler.end; 150 } 151 if (i == 2) 152 { 153 version(Dplug_ProfileUI) context.profiler.begin("resize Depth background"); 154 resizer.resizeImageDepth(_depth.toRef, _depthResized.toRef); 155 version(Dplug_ProfileUI) context.profiler.end; 156 } 157 } 158 context.globalThreadPool.parallelFor(3, &resizeOneImage); 159 160 _forceResizeUpdate = false; 161 162 version (decompressImagesLazily) 163 { 164 freeBackgroundImages(); 165 assert(_diffuse is null); 166 } 167 } 168 } 169 170 // Just blit backgrounds into dirtyRects. 171 foreach(dirtyRect; dirtyRects) 172 { 173 auto croppedDiffuseIn = _diffuseResized.toRef().cropImageRef(dirtyRect); 174 auto croppedDiffuseOut = diffuseMap.cropImageRef(dirtyRect); 175 176 auto croppedDepthIn = _depthResized.toRef().cropImageRef(dirtyRect); 177 auto croppedDepthOut = depthMap.cropImageRef(dirtyRect); 178 179 auto croppedMaterialIn = _materialResized.toRef().cropImageRef(dirtyRect); 180 auto croppedMaterialOut = materialMap.cropImageRef(dirtyRect); 181 182 croppedDiffuseIn.blitTo(croppedDiffuseOut); 183 croppedDepthIn.blitTo(croppedDepthOut); 184 croppedMaterialIn.blitTo(croppedMaterialOut); 185 } 186 } 187 188 override void reflow() 189 { 190 } 191 192 private: 193 194 // Can pass a ThreadPool in the case you want parallel image loading (optional). 195 final void loadBackgroundImagesFromStaticData(ThreadPool* threadPool) 196 { 197 auto basecolorData = cast(ubyte[])(import(baseColorPath)); 198 static if (emissivePath) 199 ubyte[] emissiveData = cast(ubyte[])(import(emissivePath)); 200 else 201 ubyte[] emissiveData = null; 202 auto materialData = cast(ubyte[])(import(materialPath)); 203 auto depthData = cast(ubyte[])(import(depthPath)); 204 205 loadBackgroundImages(basecolorData, 206 emissiveData, 207 materialData, 208 depthData, 209 threadPool); 210 } 211 212 // CTFE used here so we are allowed to use ~ 213 static immutable string baseColorPathAbs = absoluteGfxDirectory ~ baseColorPath; 214 static immutable string emissivePathAbs = emissivePath ? (absoluteGfxDirectory ~ emissivePath) : null; 215 static immutable string materialPathAbs = absoluteGfxDirectory ~ materialPath; 216 static immutable string depthPathAbs = absoluteGfxDirectory ~ depthPath; 217 static immutable string skyboxPathAbs = absoluteGfxDirectory ~ skyboxPath; 218 219 OwnedImage!RGBA _diffuse; 220 OwnedImage!RGBA _material; 221 OwnedImage!L16 _depth; 222 223 OwnedImage!RGBA _diffuseResized; 224 OwnedImage!RGBA _materialResized; 225 OwnedImage!L16 _depthResized; 226 227 /// Where pixel data is taken in the image, expressed in _background coordinates. 228 box2i _sourceRect; 229 230 /// Where it is deposited. Same size than _sourceRect. Expressed in _position coordinates. 231 box2i _destRect; 232 233 /// Offset from source to dest. 234 vec2i _offset; 235 236 /// Force resize of source image in order to display changes while editing files. 237 bool _forceResizeUpdate; 238 239 void freeBackgroundImages() 240 { 241 if (_diffuse) 242 { 243 _diffuse.destroyFree(); 244 _diffuse = null; 245 } 246 247 if (_depth) 248 { 249 _depth.destroyFree(); 250 _depth = null; 251 } 252 253 if (_material) 254 { 255 _material.destroyFree(); 256 _material = null; 257 } 258 } 259 260 version(decompressImagesLazily) 261 { 262 } 263 else 264 { 265 // Reloads images for UI development, avoid long compile round trips 266 // This saves up hours. 267 void reloadImagesAtRuntime() 268 { 269 // reading images with an absolute path since we don't know 270 // which is the current directory from the host 271 ubyte[] basecolorData = readFile(baseColorPathAbs); 272 ubyte[] emissiveData = emissivePathAbs ? readFile(emissivePathAbs) : null; 273 ubyte[] materialData = readFile(materialPathAbs); 274 ubyte[] depthData = readFile(depthPathAbs); 275 ubyte[] skyboxData = readFile(skyboxPathAbs); 276 277 if (basecolorData && materialData 278 && depthData && skyboxData) // all valid? 279 { 280 // Reload images from disk and update the UI 281 freeBackgroundImages(); 282 loadBackgroundImages(basecolorData, emissiveData, materialData, depthData, null); 283 loadSkybox(skyboxData); 284 _forceResizeUpdate = true; 285 setDirtyWhole(); 286 } 287 else 288 { 289 // Note: if you fail here, the absolute path you provided in your gui.d was incorrect. 290 // The background files cannot be loaded at runtime, and you have to fix your pathes. 291 assert(false); 292 } 293 294 // Release copy of file contents 295 freeSlice(basecolorData); 296 freeSlice(emissiveData); 297 freeSlice(materialData); 298 freeSlice(depthData); 299 freeSlice(skyboxData); 300 } 301 } 302 303 void loadBackgroundImages(ubyte[] basecolorData, 304 ubyte[] emissiveData, // this one can be null 305 ubyte[] materialData, 306 ubyte[] depthData, 307 ThreadPool* threadPool) 308 { 309 // Potentially load all 3 background images in parallel, if one threadPool is provided. 310 void loadOneImage(int i, int threadIndex) nothrow @nogc 311 { 312 ImageResizer resizer; 313 if (i == 0) 314 { 315 version(Dplug_ProfileUI) context.profiler.category("image").begin("load Diffuse background"); 316 if (emissiveData) 317 _diffuse = loadImageSeparateAlpha(basecolorData, emissiveData); 318 else 319 { 320 // fill with zero, no emissive data => zero Emissive 321 _diffuse = loadImageWithFilledAlpha(basecolorData, 0); 322 } 323 version(Dplug_ProfileUI) context.profiler.end; 324 } 325 if (i == 1) 326 { 327 version(Dplug_ProfileUI) context.profiler.begin("load Material background"); 328 _material = loadOwnedImage(materialData); 329 version(Dplug_ProfileUI) context.profiler.end; 330 } 331 if (i == 2) 332 { 333 version(Dplug_ProfileUI) context.profiler.begin("load Depth background"); 334 _depth = loadOwnedImageDepth(depthData); 335 version(Dplug_ProfileUI) context.profiler.end; 336 } 337 } 338 if (threadPool) 339 { 340 threadPool.parallelFor(3, &loadOneImage); 341 } 342 else 343 { 344 loadOneImage(0, -1); 345 loadOneImage(1, -1); 346 loadOneImage(2, -1); 347 } 348 } 349 350 void loadSkybox(ubyte[] skyboxData) 351 { 352 // Search for a pass of type PassSkyboxReflections 353 if (auto mpc = cast(MultipassCompositor) compositor()) 354 { 355 foreach(pass; mpc.passes()) 356 { 357 if (auto skyreflPass = cast(PassSkyboxReflections)pass) 358 { 359 version(Dplug_ProfileUI) context.profiler.category("image").begin("load Skybox"); 360 OwnedImage!RGBA skybox = loadOwnedImage(skyboxData); 361 skyreflPass.setSkybox(skybox); 362 version(Dplug_ProfileUI) context.profiler.end; 363 } 364 } 365 } 366 } 367 } 368