1 /** 2 * PBR rendering, custom rendering. 3 * 4 * Copyright: Copyright Auburn Sounds 2015 and later. 5 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 * Authors: Guillaume Piolat 7 */ 8 module dplug.gui.compositor; 9 10 import std.math; 11 12 import gfm.math.vector; 13 import gfm.math.box; 14 import gfm.math.matrix; 15 16 import dplug.core.vec; 17 import dplug.core.nogc; 18 import dplug.core.math; 19 20 import dplug.window.window; 21 22 import dplug.graphics.mipmap; 23 import dplug.graphics.drawex; 24 import dplug.graphics.image; 25 import dplug.graphics.view; 26 27 // Only deals with rendering tiles. 28 // If you don't like Dplug default compositing, just make another Compositor 29 // and assign the 'compositor' field in GUIGraphics. 30 // However for now mipmaps are not negotiable, they will get generated outside this compositor. 31 interface ICompositor 32 { 33 void compositeTile(ImageRef!RGBA wfb, WindowPixelFormat pf, box2i area, 34 Mipmap!RGBA diffuseMap, 35 Mipmap!RGBA materialMap, 36 Mipmap!L16 depthMap, 37 Mipmap!RGBA skybox) nothrow @nogc; 38 } 39 40 /// "Physically Based"-style rendering 41 class PBRCompositor : ICompositor 42 { 43 nothrow @nogc: 44 // light 1 used for key lighting and shadows 45 // always coming from top-right 46 vec3f light1Color; 47 48 // light 2 used for diffuse lighting 49 vec3f light2Dir; 50 vec3f light2Color; 51 52 // light 3 used for specular highlights 53 vec3f light3Dir; 54 vec3f light3Color; 55 56 float ambientLight; 57 float skyboxAmount; 58 59 this() 60 { 61 float globalLightFactor = 1.3f; 62 // defaults 63 light1Color = vec3f(0.25f, 0.25f, 0.25f) * globalLightFactor; 64 65 light2Dir = vec3f(0.0f, 1.0f, 0.1f).normalized; 66 light2Color = vec3f(0.37f, 0.37f, 0.37f) * globalLightFactor; 67 68 light3Dir = vec3f(0.0f, 1.0f, 0.1f).normalized; 69 light3Color = vec3f(0.2f, 0.2f, 0.2f) * globalLightFactor; 70 71 ambientLight = 0.0625f * globalLightFactor; 72 skyboxAmount = 0.4f * globalLightFactor; 73 74 for (int roughByte = 0; roughByte < 256; ++roughByte) 75 { 76 _exponentTable[roughByte] = 0.8f * exp( (1-roughByte / 255.0f) * 5.5f); 77 } 78 79 // Set standard tables 80 enum tableAlignment = 256; // this is necessary for asm optimization of look-up 81 _tableArea.reallocBuffer(256 * 3, tableAlignment); 82 _redTransferTable = _tableArea.ptr; 83 _greenTransferTable = _tableArea.ptr + 256; 84 _blueTransferTable = _tableArea.ptr + 512; 85 86 foreach(i; 0..256) 87 { 88 _redTransferTable[i] = cast(ubyte)i; 89 _greenTransferTable[i] = cast(ubyte)i; 90 _blueTransferTable[i] = cast(ubyte)i; 91 } 92 } 93 94 ~this() 95 { 96 enum tableAlignment = 256; 97 _tableArea.reallocBuffer(0, tableAlignment); 98 } 99 100 /// Calling this setup color correction table, with the well 101 /// known lift-gamma-gain formula. 102 void setLiftGammaGainContrast(float lift = 0.0f, float gamma = 1.0f, float gain = 1.0f, float contrast = 0.0f) 103 { 104 setLiftGammaGainContrastRGB(lift, gamma, gain, contrast, 105 lift, gamma, gain, contrast, 106 lift, gamma, gain, contrast); 107 } 108 109 /// Calling this setup color correction table, with the well 110 /// known lift-gamma-gain formula, per channel. 111 void setLiftGammaGainRGB(float rLift = 0.0f, float rGamma = 1.0f, float rGain = 1.0f, 112 float gLift = 0.0f, float gGamma = 1.0f, float gGain = 1.0f, 113 float bLift = 0.0f, float bGamma = 1.0f, float bGain = 1.0f) 114 { 115 setLiftGammaGainContrastRGB(rLift, rGamma, rGain, 0.0f, 116 gLift, gGamma, gGain, 0.0f, 117 bLift, bGamma, bGain, 0.0f); 118 } 119 120 /// Calling this setup color correction table, with the less 121 /// known lift-gamma-gain formula + contrast addition, per channel. 122 void setLiftGammaGainContrastRGB( 123 float rLift = 0.0f, float rGamma = 1.0f, float rGain = 1.0f, float rContrast = 0.0f, 124 float gLift = 0.0f, float gGamma = 1.0f, float gGain = 1.0f, float gContrast = 0.0f, 125 float bLift = 0.0f, float bGamma = 1.0f, float bGain = 1.0f, float bContrast = 0.0f) 126 { 127 static float safePow(float a, float b) nothrow @nogc 128 { 129 if (a < 0) 130 a = 0; 131 if (a > 1) 132 a = 1; 133 return a ^^ b; 134 } 135 136 for (int b = 0; b < 256; ++b) 137 { 138 float inp = b / 255.0f; 139 float outR = rGain*(inp + rLift*(1-inp)); 140 float outG = gGain*(inp + gLift*(1-inp)); 141 float outB = bGain*(inp + bLift*(1-inp)); 142 143 outR = safePow(outR, 1.0f / rGamma ); 144 outG = safePow(outG, 1.0f / gGamma ); 145 outB = safePow(outB, 1.0f / bGamma ); 146 147 if (outR < 0) 148 outR = 0; 149 if (outG < 0) 150 outG = 0; 151 if (outB < 0) 152 outB = 0; 153 if (outR > 1) 154 outR = 1; 155 if (outG > 1) 156 outG = 1; 157 if (outB > 1) 158 outB = 1; 159 160 outR = lerp!float(outR, smoothStep!float(0, 1, outR), rContrast); 161 outG = lerp!float(outG, smoothStep!float(0, 1, outG), gContrast); 162 outB = lerp!float(outB, smoothStep!float(0, 1, outB), bContrast); 163 164 _redTransferTable[b] = cast(ubyte)(0.5f + outR * 255.0f); 165 _greenTransferTable[b] = cast(ubyte)(0.5f + outG * 255.0f); 166 _blueTransferTable[b] = cast(ubyte)(0.5f + outB * 255.0f); 167 } 168 } 169 170 /// ditto 171 void setLiftGammaGainContrastRGB(mat3x4f liftGammaGainContrast) 172 { 173 auto m = liftGammaGainContrast; 174 setLiftGammaGainContrastRGB(m.c[0][0], m.c[0][1], m.c[0][2], m.c[0][3], 175 m.c[1][0], m.c[1][1], m.c[1][2], m.c[1][3], 176 m.c[2][0], m.c[2][1], m.c[2][2], m.c[2][3]); 177 } 178 179 /// Don't like this rendering? Feel free to override this method. 180 override void compositeTile(ImageRef!RGBA wfb, WindowPixelFormat pf, box2i area, 181 Mipmap!RGBA diffuseMap, 182 Mipmap!RGBA materialMap, 183 Mipmap!L16 depthMap, 184 Mipmap!RGBA skybox) 185 { 186 ushort[3][3] depthPatch = void; 187 L16*[3] depth_scan = void; 188 189 int w = diffuseMap.levels[0].w; 190 int h = diffuseMap.levels[0].h; 191 float invW = 1.0f / w; 192 float invH = 1.0f / h; 193 static immutable float div255 = 1 / 255.0f; 194 195 for (int j = area.min.y; j < area.max.y; ++j) 196 { 197 RGBA* wfb_scan = wfb.scanline(j).ptr; 198 199 // clamp to existing lines 200 { 201 OwnedImage!L16 depthLevel0 = depthMap.levels[0]; 202 for (int line = 0; line < 3; ++line) 203 { 204 int lineIndex = j - 1 + line; 205 if (lineIndex < 0) 206 lineIndex = 0; 207 if (lineIndex > h - 1) 208 lineIndex = h - 1; 209 depth_scan[line] = depthLevel0.scanline(lineIndex).ptr; 210 } 211 } 212 213 RGBA* materialScan = materialMap.levels[0].scanline(j).ptr; 214 RGBA* diffuseScan = diffuseMap.levels[0].scanline(j).ptr; 215 216 for (int i = area.min.x; i < area.max.x; ++i) 217 { 218 RGBA materialHere = materialScan[i]; 219 220 // Bypass PBR if Physical == 0 221 if (materialHere.a == 0) 222 { 223 wfb_scan[i] = diffuseScan[i]; 224 } 225 else 226 { 227 // clamp to existing columns 228 229 { 230 // Get depth for a 3x3 patch 231 // Turns out DMD like hand-unrolling:( 232 for (int k = 0; k < 3; ++k) 233 { 234 int colIndex = i - 1 + k; 235 if (colIndex < 0) 236 colIndex = 0; 237 if (colIndex > w - 1) 238 colIndex = w - 1; 239 240 depthPatch[0][k] = depth_scan.ptr[0][colIndex].l; 241 depthPatch[1][k] = depth_scan.ptr[1][colIndex].l; 242 depthPatch[2][k] = depth_scan.ptr[2][colIndex].l; 243 } 244 } 245 246 // compute normal 247 float sx = depthPatch[0][0] 248 + depthPatch[1][0] * 2 249 + depthPatch[2][0] 250 - ( depthPatch[0][2] 251 + depthPatch[1][2] * 2 252 + depthPatch[2][2]); 253 254 float sy = depthPatch[2][0] + depthPatch[2][1] * 2 + depthPatch[2][2] 255 - ( depthPatch[0][0] + depthPatch[0][1] * 2 + depthPatch[0][2]); 256 257 // this factor basically tweak normals to make the UI flatter or not 258 // if you change normal filtering, retune this 259 enum float sz = 260.0f * 257.0f / 1.8f; 260 261 vec3f normal = vec3f(sx, sy, sz); 262 normal.fastNormalize(); // this makes very, very little difference in output vs normalize 263 264 RGBA ibaseColor = diffuseScan[i]; 265 vec3f baseColor = vec3f(ibaseColor.r, ibaseColor.g, ibaseColor.b) * div255; 266 267 vec3f toEye = vec3f(0.5f - i * invW, j * invH - 0.5f, 1.0f); 268 toEye.fastNormalize(); // this makes very, very little difference in output vs normalize 269 270 vec3f color = vec3f(0.0f); 271 272 float roughness = materialHere.r * div255; 273 float metalness = materialHere.g * div255; 274 float specular = materialHere.b * div255; 275 276 // Add ambient component 277 { 278 float px = i + 0.5f; 279 float py = j + 0.5f; 280 281 float avgDepthHere = 282 ( depthMap.linearSample(1, px, py) 283 + depthMap.linearSample(2, px, py) 284 + depthMap.linearSample(3, px, py) 285 + depthMap.linearSample(4, px, py) ) * 0.25f; 286 287 float diff = depthPatch[1][1] - avgDepthHere; 288 float cavity = void; 289 if (diff >= 0) 290 cavity = 1; 291 else if (diff < -23040) 292 cavity = 0; 293 else 294 { 295 static immutable float divider23040 = 1.0f / 23040; 296 cavity = (diff + 23040) * divider23040; 297 } 298 if (cavity > 0) 299 color += baseColor * (cavity * ambientLight); 300 } 301 302 // cast shadows, ie. enlight what isn't in shadows 303 { 304 enum float fallOff = 0.78f; 305 306 int samples = 11; 307 308 static immutable float[11] weights = 309 [ 310 1.0f, 311 fallOff, 312 fallOff ^^ 2, 313 fallOff ^^ 3, 314 fallOff ^^ 4, 315 fallOff ^^ 5, 316 fallOff ^^ 6, 317 fallOff ^^ 7, 318 fallOff ^^ 8, 319 fallOff ^^ 9, 320 fallOff ^^ 10 321 ]; 322 323 enum float totalWeights = (1.0f - (fallOff ^^ 11)) / (1.0f - fallOff) - 1; 324 enum float invTotalWeights = 1 / totalWeights; 325 326 float lightPassed = 0.0f; 327 328 OwnedImage!L16 depthLevel0 = depthMap.levels[0]; 329 330 int depthHere = depthPatch[1][1]; 331 for (int sample = 1; sample < samples; ++sample) 332 { 333 int x = i + sample; 334 if (x >= w) 335 x = w - 1; 336 int y = j - sample; 337 if (y < 0) 338 y = 0; 339 int z = depthHere + sample; 340 int diff = z - depthLevel0.scanline(y)[x].l; // FUTURE: use pointer offsets here instead of opIndex 341 342 float contrib = void; 343 if (diff >= 0) 344 contrib = 1; 345 else if (diff < -15360) 346 { 347 contrib = 0; 348 continue; 349 } 350 else 351 { 352 static immutable float divider15360 = 1.0f / 15360; 353 contrib = (diff + 15360) * divider15360; 354 } 355 356 lightPassed += contrib * weights[sample]; 357 } 358 color += baseColor * light1Color * (lightPassed * invTotalWeights); 359 } 360 361 // secundary light 362 { 363 float diffuseFactor = 0.5f + 0.5f * dot(normal, light2Dir);// + roughness; 364 365 diffuseFactor = /*cavity * */ linmap!float(diffuseFactor, 0.24f - roughness * 0.5f, 1, 0, 1.0f); 366 367 if (diffuseFactor > 0) 368 color += baseColor * light2Color * diffuseFactor; 369 } 370 371 // specular reflection 372 if (specular != 0) 373 { 374 vec3f lightReflect = reflect(-light3Dir, normal); 375 float specularFactor = dot(toEye, lightReflect); 376 if (specularFactor > 1e-3f) 377 { 378 float exponent = _exponentTable[materialHere.r]; 379 specularFactor = specularFactor ^^ exponent; 380 float roughFactor = 10 * (1.0f - roughness) * (1 - metalness * 0.5f); 381 specularFactor = specularFactor * roughFactor; 382 if (specularFactor != 0) 383 color += baseColor * light3Color * (specularFactor * specular); 384 } 385 } 386 387 // skybox reflection (use the same shininess as specular) 388 if (metalness != 0) 389 { 390 vec3f pureReflection = reflect(toEye, normal); 391 392 float skyx = 0.5f + ((0.5f - pureReflection.x *0.5f) * (skybox.width - 1)); 393 float skyy = 0.5f + ((0.5f + pureReflection.y *0.5f) * (skybox.height - 1)); 394 395 // 2nd order derivatives 396 float depthDX = depthPatch[2][0] + depthPatch[2][1] + depthPatch[2][2] 397 + depthPatch[0][0] + depthPatch[0][1] + depthPatch[0][2] 398 - 2 * (depthPatch[1][0] + depthPatch[1][1] + depthPatch[1][2]); 399 400 float depthDY = depthPatch[0][2] + depthPatch[1][2] + depthPatch[2][2] 401 + depthPatch[0][0] + depthPatch[1][0] + depthPatch[2][0] 402 - 2 * (depthPatch[0][1] + depthPatch[1][1] + depthPatch[2][1]); 403 404 depthDX *= (1 / 256.0f); 405 depthDY *= (1 / 256.0f); 406 407 float depthDerivSqr = depthDX * depthDX + depthDY * depthDY; 408 float indexDeriv = depthDerivSqr * skybox.width * skybox.height; 409 410 // cooking here 411 // log2 scaling + threshold 412 float mipLevel = 0.5f * fastlog2(1.0f + indexDeriv * 0.00001f) + 6 * roughness; 413 414 vec3f skyColor = skybox.linearMipmapSample(mipLevel, skyx, skyy).rgb * (div255 * metalness * skyboxAmount); 415 color += skyColor * baseColor; 416 } 417 418 // Add light emitted by neighbours 419 { 420 float ic = i + 0.5f; 421 float jc = j + 0.5f; 422 423 // Get alpha-premultiplied, avoids to have to do alpha-aware mipmapping 424 vec4f colorLevel1 = diffuseMap.linearSample(1, ic, jc); 425 vec4f colorLevel2 = diffuseMap.linearSample(2, ic, jc); 426 vec4f colorLevel3 = diffuseMap.linearSample(3, ic, jc); 427 vec4f colorLevel4 = diffuseMap.linearSample(4, ic, jc); 428 vec4f colorLevel5 = diffuseMap.linearSample(5, ic, jc); 429 430 vec4f emitted = colorLevel1 * 0.00117647f; 431 emitted += colorLevel2 * 0.00176471f; 432 emitted += colorLevel3 * 0.00147059f; 433 emitted += colorLevel4 * 0.00088235f; 434 emitted += colorLevel5 * 0.00058823f; 435 color += emitted.rgb; 436 } 437 438 // Partial blending of PBR with diffuse 439 if (materialHere.a != 255) 440 { 441 float physical = materialHere.a * div255; 442 color += (baseColor - color) * (1 - physical); 443 } 444 445 // Show normals 446 // color = normal;//vec3f(0.5f) + normal * 0.5f; 447 448 // Show depth 449 { 450 // float depthColor = depthPatch[1][1] / 65535.0f; 451 // color = vec3f(depthColor); 452 } 453 454 // Show diffuse 455 //color = baseColor; 456 457 // color = toEye; 458 //color = vec3f(cavity); 459 assert(color.x >= 0); 460 assert(color.y >= 0); 461 assert(color.z >= 0); 462 463 if (color.x > 1) 464 color.x = 1; 465 if (color.y > 1) 466 color.y = 1; 467 if (color.z > 1) 468 color.z = 1; 469 470 int r = cast(int)(color.x * 255.99f); 471 int g = cast(int)(color.y * 255.99f); 472 int b = cast(int)(color.z * 255.99f); 473 474 // write composited color 475 wfb_scan[i] = RGBA(cast(ubyte)r, cast(ubyte)g, cast(ubyte)b, 255); 476 } 477 } 478 } 479 480 // Look-up table for color-correction and ordering of output 481 { 482 ubyte* rgbTable = _redTransferTable; 483 484 final switch (pf) with (WindowPixelFormat) 485 { 486 case ARGB8: 487 applyColorCorrectionARGB8(wfb, area, rgbTable); 488 break; 489 490 case BGRA8: 491 applyColorCorrectionBGRA8(wfb, area, rgbTable); 492 break; 493 494 case RGBA8: 495 applyColorCorrectionRGBA8(wfb, area, rgbTable); 496 break; 497 } 498 } 499 } 500 501 private: 502 // Assign those to use lookup tables. 503 ubyte[] _tableArea = null; 504 ubyte* _redTransferTable = null; 505 ubyte* _greenTransferTable = null; 506 ubyte* _blueTransferTable = null; 507 508 float[256] _exponentTable; 509 } 510 511 private: 512 513 // cause smoothStep wasn't needed 514 float ctLinearStep(float a, float b)(float t) pure nothrow @nogc 515 { 516 if (t <= a) 517 return 0.0f; 518 else if (t >= b) 519 return 1.0f; 520 else 521 { 522 static immutable divider = 1.0f / (b - a); 523 return (t - a) * divider; 524 } 525 } 526 527 private: 528 529 // cause smoothStep wasn't needed 530 float linearStep(float a, float b, float t) pure nothrow @nogc 531 { 532 if (t <= a) 533 return 0.0f; 534 else if (t >= b) 535 return 1.0f; 536 else 537 { 538 float divider = 1.0f / (b - a); 539 return (t - a) * divider; 540 } 541 } 542 543 // log2 approximation by Laurent de Soras 544 // http://www.flipcode.com/archives/Fast_log_Function.shtml 545 float fastlog2(float val) pure nothrow @nogc 546 { 547 union fi_t 548 { 549 int i; 550 float f; 551 } 552 553 fi_t fi; 554 fi.f = val; 555 int x = fi.i; 556 int log_2 = ((x >> 23) & 255) - 128; 557 x = x & ~(255 << 23); 558 x += 127 << 23; 559 fi.i = x; 560 return fi.f + log_2; 561 } 562 563 // Apply color correction and convert RGBA8 to ARGB8 564 void applyColorCorrectionARGB8(ImageRef!RGBA wfb, const(box2i) area, const(ubyte*) rgbTable) pure nothrow @nogc 565 { 566 for (int j = area.min.y; j < area.max.y; ++j) 567 { 568 ubyte* wfb_scan = cast(ubyte*)wfb.scanline(j).ptr; 569 for (int i = area.min.x; i < area.max.x; ++i) 570 { 571 ubyte r = wfb_scan[4*i]; 572 ubyte g = wfb_scan[4*i+1]; 573 ubyte b = wfb_scan[4*i+2]; 574 wfb_scan[4*i] = 255; 575 wfb_scan[4*i+1] = rgbTable[r]; 576 wfb_scan[4*i+2] = rgbTable[g+256]; 577 wfb_scan[4*i+3] = rgbTable[b+512]; 578 } 579 } 580 } 581 582 // Apply color correction and convert RGBA8 to BGRA8 583 void applyColorCorrectionBGRA8(ImageRef!RGBA wfb, const(box2i) area, const(ubyte*) rgbTable) pure nothrow @nogc 584 { 585 int width = area.width(); 586 for (int j = area.min.y; j < area.max.y; ++j) 587 { 588 ubyte* wfb_scan = cast(ubyte*)wfb.scanline(j).ptr; 589 for (int i = area.min.x; i < area.max.x; ++i) 590 { 591 ubyte r = wfb_scan[4*i]; 592 ubyte g = wfb_scan[4*i+1]; 593 ubyte b = wfb_scan[4*i+2]; 594 wfb_scan[4*i] = rgbTable[b+512]; 595 wfb_scan[4*i+1] = rgbTable[g+256]; 596 wfb_scan[4*i+2] = rgbTable[r]; 597 wfb_scan[4*i+3] = 255; 598 } 599 } 600 } 601 602 // Apply color correction and do nothing about color order 603 void applyColorCorrectionRGBA8(ImageRef!RGBA wfb, const(box2i) area, const(ubyte*) rgbTable) pure nothrow @nogc 604 { 605 for (int j = area.min.y; j < area.max.y; ++j) 606 { 607 ubyte* wfb_scan = cast(ubyte*)wfb.scanline(j).ptr; 608 for (int i = area.min.x; i < area.max.x; ++i) 609 { 610 ubyte r = wfb_scan[4*i]; 611 ubyte g = wfb_scan[4*i+1]; 612 ubyte b = wfb_scan[4*i+2]; 613 wfb_scan[4*i] = rgbTable[r]; 614 wfb_scan[4*i+1] = rgbTable[g+256]; 615 wfb_scan[4*i+2] = rgbTable[b+512]; 616 wfb_scan[4*i+3] = 255; 617 } 618 } 619 }