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 }