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