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 }