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 // If this version is enabled, you could press ENTER to 89 // reload the backgrounds. Do not ship this! 90 version(Dplug_EnterReloadBackgrounds) 91 { 92 override bool onKeyDown(Key key) 93 { 94 if (super.onKeyDown(key)) 95 return true; 96 97 version(decompressImagesLazily) 98 { 99 } 100 else 101 { 102 if (key == Key.enter) 103 { 104 reloadImagesAtRuntime(); 105 return true; 106 } 107 } 108 109 return false; 110 } 111 } 112 113 override void onDrawPBR(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects) 114 { 115 // Resize resource to match _position 116 { 117 int W = position.width; 118 int H = position.height; 119 120 if (_forceResizeUpdate || _diffuseResized.w != W || _diffuseResized.h != H) 121 { 122 // Decompress images lazily for this size 123 124 version (decompressImagesLazily) 125 { 126 assert(_diffuse is null); 127 128 // No thread pool, load images one by one. 129 loadBackgroundImagesFromStaticData(context.globalThreadPool); 130 } 131 132 _diffuseResized.size(W, H); 133 _materialResized.size(W, H); 134 _depthResized.size(W, H); 135 136 // Potentially resize all 3 backgrounds in parallel 137 void resizeOneImage(int i, int threadIndex) nothrow @nogc 138 { 139 ImageResizer resizer; 140 if (i == 0) 141 { 142 version(Dplug_ProfileUI) context.profiler.begin("resize Diffuse background"); 143 resizer.resizeImageDiffuse(_diffuse.toRef, _diffuseResized.toRef); 144 version(Dplug_ProfileUI) context.profiler.end; 145 } 146 if (i == 1) 147 { 148 version(Dplug_ProfileUI) context.profiler.begin("resize Material background"); 149 resizer.resizeImageMaterial(_material.toRef, _materialResized.toRef); 150 version(Dplug_ProfileUI) context.profiler.end; 151 } 152 if (i == 2) 153 { 154 version(Dplug_ProfileUI) context.profiler.begin("resize Depth background"); 155 resizer.resizeImageDepth(_depth.toRef, _depthResized.toRef); 156 version(Dplug_ProfileUI) context.profiler.end; 157 } 158 } 159 context.globalThreadPool.parallelFor(3, &resizeOneImage); 160 161 _forceResizeUpdate = false; 162 163 version (decompressImagesLazily) 164 { 165 freeBackgroundImages(); 166 assert(_diffuse is null); 167 } 168 } 169 } 170 171 // Just blit backgrounds into dirtyRects. 172 foreach(dirtyRect; dirtyRects) 173 { 174 auto croppedDiffuseIn = _diffuseResized.toRef().cropImageRef(dirtyRect); 175 auto croppedDiffuseOut = diffuseMap.cropImageRef(dirtyRect); 176 177 auto croppedDepthIn = _depthResized.toRef().cropImageRef(dirtyRect); 178 auto croppedDepthOut = depthMap.cropImageRef(dirtyRect); 179 180 auto croppedMaterialIn = _materialResized.toRef().cropImageRef(dirtyRect); 181 auto croppedMaterialOut = materialMap.cropImageRef(dirtyRect); 182 183 croppedDiffuseIn.blitTo(croppedDiffuseOut); 184 croppedDepthIn.blitTo(croppedDepthOut); 185 croppedMaterialIn.blitTo(croppedMaterialOut); 186 } 187 } 188 189 override void reflow() 190 { 191 } 192 193 private: 194 195 // Can pass a ThreadPool in the case you want parallel image loading (optional). 196 final void loadBackgroundImagesFromStaticData(ThreadPool* threadPool) 197 { 198 auto basecolorData = cast(ubyte[])(import(baseColorPath)); 199 static if (emissivePath) 200 ubyte[] emissiveData = cast(ubyte[])(import(emissivePath)); 201 else 202 ubyte[] emissiveData = null; 203 auto materialData = cast(ubyte[])(import(materialPath)); 204 auto depthData = cast(ubyte[])(import(depthPath)); 205 206 loadBackgroundImages(basecolorData, 207 emissiveData, 208 materialData, 209 depthData, 210 threadPool); 211 } 212 213 // CTFE used here so we are allowed to use ~ 214 static immutable string baseColorPathAbs = absoluteGfxDirectory ~ baseColorPath; 215 static immutable string emissivePathAbs = emissivePath ? (absoluteGfxDirectory ~ emissivePath) : null; 216 static immutable string materialPathAbs = absoluteGfxDirectory ~ materialPath; 217 static immutable string depthPathAbs = absoluteGfxDirectory ~ depthPath; 218 static immutable string skyboxPathAbs = absoluteGfxDirectory ~ skyboxPath; 219 220 OwnedImage!RGBA _diffuse; 221 OwnedImage!RGBA _material; 222 OwnedImage!L16 _depth; 223 224 OwnedImage!RGBA _diffuseResized; 225 OwnedImage!RGBA _materialResized; 226 OwnedImage!L16 _depthResized; 227 228 /// Where pixel data is taken in the image, expressed in _background coordinates. 229 box2i _sourceRect; 230 231 /// Where it is deposited. Same size than _sourceRect. Expressed in _position coordinates. 232 box2i _destRect; 233 234 /// Offset from source to dest. 235 vec2i _offset; 236 237 /// Force resize of source image in order to display changes while editing files. 238 bool _forceResizeUpdate; 239 240 void freeBackgroundImages() 241 { 242 if (_diffuse) 243 { 244 _diffuse.destroyFree(); 245 _diffuse = null; 246 } 247 248 if (_depth) 249 { 250 _depth.destroyFree(); 251 _depth = null; 252 } 253 254 if (_material) 255 { 256 _material.destroyFree(); 257 _material = null; 258 } 259 } 260 261 version(decompressImagesLazily) 262 { 263 } 264 else 265 { 266 // Reloads images for UI development, avoid long compile round trips 267 // This saves up hours. 268 void reloadImagesAtRuntime() 269 { 270 // reading images with an absolute path since we don't know 271 // which is the current directory from the host 272 ubyte[] basecolorData = readFile(baseColorPathAbs); 273 ubyte[] emissiveData = emissivePathAbs ? readFile(emissivePathAbs) : null; 274 ubyte[] materialData = readFile(materialPathAbs); 275 ubyte[] depthData = readFile(depthPathAbs); 276 ubyte[] skyboxData = readFile(skyboxPathAbs); 277 278 if (basecolorData && materialData 279 && depthData && skyboxData) // all valid? 280 { 281 // Reload images from disk and update the UI 282 freeBackgroundImages(); 283 loadBackgroundImages(basecolorData, emissiveData, materialData, depthData, null); 284 loadSkybox(skyboxData); 285 _forceResizeUpdate = true; 286 setDirtyWhole(); 287 } 288 else 289 { 290 // Note: if you fail here, the absolute path you provided in your gui.d was incorrect. 291 // The background files cannot be loaded at runtime, and you have to fix your pathes. 292 assert(false); 293 } 294 295 // Release copy of file contents 296 freeSlice(basecolorData); 297 freeSlice(emissiveData); 298 freeSlice(materialData); 299 freeSlice(depthData); 300 freeSlice(skyboxData); 301 } 302 } 303 304 void loadBackgroundImages(ubyte[] basecolorData, 305 ubyte[] emissiveData, // this one can be null 306 ubyte[] materialData, 307 ubyte[] depthData, 308 ThreadPool* threadPool) 309 { 310 // Potentially load all 3 background images in parallel, if one threadPool is provided. 311 void loadOneImage(int i, int threadIndex) nothrow @nogc 312 { 313 ImageResizer resizer; 314 if (i == 0) 315 { 316 version(Dplug_ProfileUI) context.profiler.category("image").begin("load Diffuse background"); 317 if (emissiveData) 318 _diffuse = loadImageSeparateAlpha(basecolorData, emissiveData); 319 else 320 { 321 // fill with zero, no emissive data => zero Emissive 322 _diffuse = loadImageWithFilledAlpha(basecolorData, 0); 323 } 324 version(Dplug_ProfileUI) context.profiler.end; 325 } 326 if (i == 1) 327 { 328 version(Dplug_ProfileUI) context.profiler.begin("load Material background"); 329 _material = loadOwnedImage(materialData); 330 version(Dplug_ProfileUI) context.profiler.end; 331 } 332 if (i == 2) 333 { 334 version(Dplug_ProfileUI) context.profiler.begin("load Depth background"); 335 _depth = loadOwnedImageDepth(depthData); 336 version(Dplug_ProfileUI) context.profiler.end; 337 } 338 } 339 if (threadPool) 340 { 341 threadPool.parallelFor(3, &loadOneImage); 342 } 343 else 344 { 345 loadOneImage(0, -1); 346 loadOneImage(1, -1); 347 loadOneImage(2, -1); 348 } 349 } 350 351 void loadSkybox(ubyte[] skyboxData) 352 { 353 // Search for a pass of type PassSkyboxReflections 354 if (auto mpc = cast(MultipassCompositor) compositor()) 355 { 356 foreach(pass; mpc.passes()) 357 { 358 if (auto skyreflPass = cast(PassSkyboxReflections)pass) 359 { 360 version(Dplug_ProfileUI) context.profiler.category("image").begin("load Skybox"); 361 OwnedImage!RGBA skybox = loadOwnedImage(skyboxData); 362 skyreflPass.setSkybox(skybox); 363 version(Dplug_ProfileUI) context.profiler.end; 364 } 365 } 366 } 367 } 368 } 369