1 /**
2 A PBR knob with texture.
3 
4 Copyright: Guillaume Piolat 2019.
5 License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 */
7 module dplug.pbrwidgets.imageknob;
8 
9 import std.math: PI_2, cos, sin;
10 
11 import inteli.smmintrin;
12 
13 import dplug.math.vector;
14 import dplug.math.box;
15 
16 import dplug.core.nogc;
17 import dplug.gui.element;
18 import dplug.pbrwidgets.knob;
19 import dplug.client.params;
20 
21 import gamut;
22 
23 // FUTURE: ABDME_8 shall disappear.
24 nothrow:
25 @nogc:
26 
27 // Removing this is NOT READY.
28 // See: https://github.com/AuburnSounds/Dplug/issues/791
29 // TODO put high-quality mipmaps in KnobImage, interpolate those.
30 version = KnobImage_resizedInternal;
31 
32 enum KnobImageType
33 {
34     /// Legacy KnobImage format.
35     ABDME_8, 
36 
37     /// New KnobImage format.
38     BADAMA_16
39 }
40 
41 enum KnobInterpolationType
42 {
43     linear, 
44     cubic
45 }
46 
47 /// Type of image being used for Knob graphics.
48 /// It used to be a one level deep Mipmap (ie. a flat image with sampling capabilities).
49 /// It is now a regular `OwnedImage` since it is resized in `reflow()`.
50 /// Use it an opaque type: its definition can change.
51 class KnobImage
52 {
53 nothrow @nogc:
54     /// Type of KnobImage.
55     KnobImageType type;    
56 
57     /// A gamut image, must be RGBA, can be 8-bit or 16-bit.
58     Image image;
59 
60     bool isLegacy()
61     {
62         return type == KnobImageType.ABDME_8;
63     }
64 }
65 
66 /// Loads a knob image and rearrange channels to be fit to pass to `UIImageKnob`.
67 /// Warning: the returned `KnobImage` should be destroyed by the caller with `destroyFree`.
68 /// Note: internal resizing does not preserve aspect ratio exactly for 
69 ///       approximate scaled rectangles.
70 ///
71 /// # ABDME_8 format (legacy 8-bit depth knobs)
72 ///
73 /// The input format of such an image is an an arrangement of squares:
74 ///
75 ///         h              h           h            h         h
76 ///   +------------+------------+------------+------------+-----------+
77 ///   |            |            |            |            |           |
78 ///   |  alpha     |  basecolor |   depth    |  material  |  emissive |
79 /// h |  grayscale |     RGB    |  grayscale |    RMS     | grayscale |
80 ///   |  (R used)  |            |(sum of RGB)|            | (R used)  |
81 ///   |            |            |            |            |           |
82 ///   +------------+------------+------------+------------+-----------+
83 ///
84 /// This format is extended so that:
85 /// - the emissive component is copied into the diffuse channel to form a full RGBA quad,
86 /// - same for material with the physical channel, which is assumed to be always "full physical"
87 ///
88 /// Recommended format: PNG, for example a 230x46 24-bit image.
89 /// Note that such an image is formatted and resized in `reflow` before use.
90 ///
91 ///
92 /// # BADAMA_16 format (new 16-bit depth + more alpha channels)
93 ///
94 ///           h           h            h      
95 ///   +------------+------------+------------+
96 ///   |            |            |            |
97 ///   |  basecolor |   depth    |  material  |
98 /// h |    RGB     | (G used)   |    RMS     |
99 ///   |     +      |     +      |     +      |
100 ///   |   alpha    |   alpha    |   alpha    |
101 ///   +------------+------------+------------+
102 ///
103 /// Recommended format: QOIX, for example a 512x128 16-bit QOIX image.
104 /// Note that such an image is formatted and resized in `reflow` before use.
105 ///
106 /// 8-bit images are considered ABDME_8, and 16-bit images are considered BADAMA_16.
107 KnobImage loadKnobImage(in void[] data)
108 {
109     // If the input image is 8-bit, this is assumed to be in ABDME_8 format.
110     // Else this is assumed to be in BADAMA_16 format.
111     KnobImage res = mallocNew!KnobImage;
112     res.image.loadFromMemory(data, LOAD_RGB | LOAD_ALPHA | LOAD_NO_PREMUL); // Force RGB and Alpha channel.
113     if (res.image.isError)
114     {
115         return null;
116     }
117 
118     if (res.image.isFP32())
119         res.image.convertTo16Bit();
120 
121     res.type = res.image.is8Bit() ? KnobImageType.ABDME_8 : KnobImageType.BADAMA_16;
122 
123     int h = res.image.height;
124 
125     if (res.type == KnobImageType.ABDME_8)
126     {
127         assert(res.image.type == PixelType.rgba8);
128 
129         // Expected dimension is 5H x H
130         assert(res.image.width == res.image.height * 5);
131 
132         for (int y = 0; y < h; ++y)
133         {
134             RGBA[] line = cast(RGBA[]) res.image.scanline(y);
135 
136             RGBA[] basecolor = line[h..2*h];
137             RGBA[] material = line[3*h..4*h];
138             RGBA[] emissive = line[4*h..5*h];
139 
140             for (int x = 0; x < h; ++x)
141             {
142                 // Put emissive red channel into the alpha channel of base color
143                 basecolor[x].a = emissive[x].r;
144 
145                 // Fills unused with 255
146                 material[x].a = 255;
147             }
148         }
149     }
150     else
151     {
152         assert(res.image.type == PixelType.rgba16);
153 
154         // Expected dimension is 3H x H
155         assert(res.image.width == res.image.height * 3);
156 
157         // The whole image is alpha-premultiplied.
158         for (int y = 0; y < h; ++y)
159         {
160             ushort[] scan = cast(ushort[]) res.image.scanline(y);
161             for (int x = 0; x < 3*h; ++x)
162             {
163                 uint R = scan[4*x+0];
164                 uint G = scan[4*x+1];
165                 uint B = scan[4*x+2];
166                 uint A = scan[4*x+3];
167                 R = (R * A + 32768) / 65535;
168                 G = (G * A + 32768) / 65535;
169                 B = (B * A + 32768) / 65535;
170                 scan[4 * x + 0] = cast(ushort)R;
171                 scan[4 * x + 1] = cast(ushort)G;
172                 scan[4 * x + 2] = cast(ushort)B;
173                 scan[4 * x + 3] = cast(ushort)A;
174             }
175         }
176     }
177 
178     return res;
179 }
180 
181 
182 /// UIKnob which replace the knob part by a rotated PBR image.
183 class UIImageKnob : UIKnob
184 {
185 public:
186 nothrow:
187 @nogc:
188 
189     /// If `true`, diffuse data is blended in the diffuse map using alpha information.
190     /// If `false`, diffuse is left untouched.
191     @ScriptProperty bool drawToDiffuse = true;
192 
193     /// If `true`, depth data is blended in the depth map using alpha information.
194     /// If `false`, depth is left untouched.
195     @ScriptProperty bool drawToDepth = true;
196 
197     /// If `true`, material data is blended in the material map using alpha information.
198     /// If `false`, material is left untouched.
199     @ScriptProperty bool drawToMaterial = true;
200 
201     /// Amount of static emissive energy to the Emissive channel.
202     @ScriptProperty ubyte emissive = 0;
203 
204     /// Amount of static emissive energy to add when mouse is over, but not dragging.
205     @ScriptProperty ubyte emissiveHovered = 0;
206 
207     /// Amount of static emissive energy to add when mouse is over, but not dragging.
208     @ScriptProperty ubyte emissiveDragged = 0;
209 
210     /// Only used in non-legacy mode (BADAMA_16 format). The texture is oversampled on resize, so that rotated image is sharper.
211     /// This costs more CPU and memory, for better visual results.
212     /// But this is also dangerous and not always better! Try it on a big screen preferably.
213     @ScriptProperty float oversampleTexture = 1.0f;
214 
215     /// Texture interpolaton used, only for non-legacy mode (BADAMA_16 format). Try it on a big screen preferably.
216     @ScriptProperty KnobInterpolationType diffuseInterpolation = KnobInterpolationType.linear;
217 
218     ///ditto
219     @ScriptProperty KnobInterpolationType depthInterpolation = KnobInterpolationType.linear;
220 
221     ///ditto
222     @ScriptProperty KnobInterpolationType materialInterpolation = KnobInterpolationType.linear;
223 
224     /// `knobImage` should have been loaded with `loadKnobImage`.
225     /// Warning: `knobImage` must outlive the knob, it is borrowed.
226     this(UIContext context, KnobImage knobImage, Parameter parameter)
227     {
228         super(context, parameter);
229         _knobImage = knobImage;
230         
231         if (knobImage.isLegacy())
232         {
233             _tempBuf = mallocNew!(OwnedImage!L16);
234             _tempBufRGBA = mallocNew!(OwnedImage!RGBA);
235             _alphaTexture = mallocNew!(Mipmap!L16);
236             _depthTexture = mallocNew!(Mipmap!L16);
237             _diffuseTexture = mallocNew!(Mipmap!RGBA);
238             _materialTexture = mallocNew!(Mipmap!RGBA);
239         }
240         else
241         {
242             version(KnobImage_resizedInternal)
243             {
244                 _resizedImage = mallocNew!(Mipmap!RGBA16);
245             }
246         }
247     }
248 
249     ~this()
250     {
251         version(KnobImage_resizedInternal)
252             _resizedImage.destroyFree();
253         _tempBuf.destroyFree();
254         _tempBufRGBA.destroyFree();
255         _alphaTexture.destroyFree();
256         _depthTexture.destroyFree();
257         _diffuseTexture.destroyFree();
258         _materialTexture.destroyFree();
259     }
260 
261 
262     enum numMipLevels = 1;
263 
264     override void reflow()
265     {
266         int numTiles = _knobImage.type == KnobImageType.ABDME_8 ? 5 : 3;
267 
268         // This has been checked before in `loadKnobImage`.
269         assert(_knobImage.image.width % numTiles == 0);
270         int SH = _knobImage.image.width / numTiles;
271         assert(_knobImage.image.height == SH);
272 
273         auto resizer = context.globalImageResizer;
274 
275        
276         if (_knobImage.isLegacy())
277         {
278             // Things are resized towards DW x DH textures.
279             int DW = position.width;
280             int DH = position.height; 
281 
282             _tempBuf.size(SH, SH);
283             _tempBufRGBA.size(SH, SH);
284             _alphaTexture.size(numMipLevels, DW, DH);
285             _depthTexture.size(numMipLevels, DW, DH);
286             _diffuseTexture.size(numMipLevels, DW, DH);
287             _materialTexture.size(numMipLevels, DW, DH);
288 
289             // 1. Fill the alpha textures.
290             {
291                 // Extends alpha to 16-bit, resize it to destination size in _alphaTexture.
292                 ImageRef!L16 tempAlpha = _tempBuf.toRef();
293                 ImageRef!L16 destAlpha = _alphaTexture.levels[0].toRef;
294                 for (int y = 0; y < SH; ++y)
295                 {
296                     ubyte* scan = cast(ubyte*)(_knobImage.image.scanptr(y));
297                     for (int x = 0; x < SH; ++x)
298                     {
299                         ushort alpha16 = scan[4*x] * 257; // take red channel as alpha
300                         tempAlpha[x, y] = L16(alpha16);
301                     }
302                 }
303                 resizer.resizeImageGeneric(tempAlpha, destAlpha);
304             }
305        
306             // 2. Fill the depth texture.
307             {
308                 ImageRef!L16 tempDepth = _tempBuf.toRef();
309                 ImageRef!L16 destDepth = _depthTexture.levels[0].toRef;
310                 for (int y = 0; y < SH; ++y)
311                 {
312                     L16* outScan = tempDepth.scanline(y).ptr;
313 
314                     ubyte* scan = cast(ubyte*)(_knobImage.image.scanptr(y)) + SH * 4 * 2; // take depth rectangle
315 
316                     // Extends depth to 16-bit, resize it to destination size in _depthTexture.
317                     for (int x = 0; x < SH; ++x)
318                     {
319                         ushort depth = cast(ushort)(0.5f + (257 * (scan[4*x] + scan[4*x+1] + scan[4*x+2]) / 3.0f));
320                         outScan[x] = L16(depth);
321                     }
322                 }
323 
324                 // Note: different resampling kernal for depth, to smooth it. 
325                 //       Slightly more serene to look at.
326                 resizer.resizeImageDepth(tempDepth, destDepth); 
327             }
328 
329             // 3. Prepare the diffuse texture.
330             // Resize diffuse+emissive in _diffuseTexture. Note that emissive channel was fed before.
331 
332 
333             void prepareDiffuseMaterialTexture(int offsetX, ImageRef!RGBA outResizedRGBAImage)
334             {
335                 for (int y = 0; y < SH; ++y)
336                 {              
337                     RGBA* scan = cast(RGBA*)(_knobImage.image.scanptr(y)) + SH*offsetX;
338                     for (int x = 0; x < SH; ++x)
339                     {
340                         _tempBufRGBA[x, y] = scan[x]; // PERF: use the input image directly?
341                     }
342                 }
343                 resizer.resizeImageDiffuse(_tempBufRGBA.toRef, outResizedRGBAImage);
344             }
345 
346             int diffuseRect = _knobImage.isLegacy() ? 1 : 0;
347             prepareDiffuseMaterialTexture(diffuseRect, _diffuseTexture.levels[0].toRef);
348 
349             // 4. Similarly, prepare material in _materialTexture.
350             // 4th channel contain garbage.
351             int materialRect = _knobImage.isLegacy() ? 3 : 2;
352             prepareDiffuseMaterialTexture(materialRect, _materialTexture.levels[0].toRef);
353         }
354         else
355         {
356             version(KnobImage_resizedInternal)
357             {
358                 lazyResizeBADAMA16(resizer);
359             }
360         }
361     }
362 
363     version(KnobImage_resizedInternal)
364     {
365 
366         void lazyResizeBADAMA16(ImageResizer* resizer)
367         {
368             // TODO: might as well keep the source pixel ratio
369             int textureWidth = cast(int)(0.5f + oversampleTexture * position.width);
370             int textureHeight = cast(int)(0.5f + oversampleTexture * position.height);
371 
372             // resize needed?
373             if (textureWidth != _textureWidth || textureHeight != _textureHeight)
374             {
375                 ImageResizer spareResizer; // if global resize not available, use one on stack.
376 
377                 if (resizer is null)
378                 {
379                     resizer = &spareResizer;
380                 }
381 
382                 _textureWidth = textureWidth;
383                 _textureHeight = textureHeight;
384 
385                 int numTiles = 3;
386                 int DW = _textureWidth;
387                 int DH = _textureHeight;
388                 int SH = _knobImage.image.width / numTiles;
389 
390                 _resizedImage.size(numMipLevels, DW*numTiles, DH);
391 
392                 ImageRef!RGBA16 input = _knobImage.image.getImageRef!RGBA16();
393                 ImageRef!RGBA16 resized = _resizedImage.levels[0].toRef;
394 
395                 // Resize each of the 3 subrect independently, to avoid strange pixel offset polluting
396 
397                 // Resize the premultiplied images to _resizedImage.
398                 resizer.resizeImageDiffuseWithAlphaPremul(input.cropImageRef(rectangle(0, 0, SH, SH)), 
399                                                           resized.cropImageRef(rectangle(0, 0, DW, DH)));
400                 resizer.resizeImageDepthWithAlphaPremul(input.cropImageRef(rectangle(SH, 0, SH, SH)), 
401                                                         resized.cropImageRef(rectangle(DW, 0, DW, DH)));
402                 resizer.resizeImageMaterialWithAlphaPremul(input.cropImageRef(rectangle(SH*2, 0, SH, SH)), 
403                                                            resized.cropImageRef(rectangle(DW*2, 0, DW, DH)));
404             }
405         }
406     }
407 
408     override void drawKnob(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects)
409     {
410         bool legacy = _knobImage.isLegacy();
411 
412         // This is just to enable scripting of `oversampleTe5xture`.
413         if (!legacy)
414         {
415             version(KnobImage_resizedInternal)
416                 lazyResizeBADAMA16(null);
417         }
418 
419         float radius = getRadius();
420         vec2f center = getCenter();
421         float valueAngle = getValueAngle() + PI_2;
422         float cosa = cos(valueAngle);
423         float sina = sin(valueAngle);
424 
425 
426 
427         // Note: slightly incorrect, since our resize in reflow doesn't exactly preserve aspect-ratio
428         vec2f rotate(vec2f v) pure nothrow @nogc
429         {
430             return vec2f(v.x * cosa + v.y * sina, 
431                          v.y * cosa - v.x * sina);
432         }
433 
434         int emissiveOffset = emissive;
435         if (isDragged)
436             emissiveOffset = emissiveDragged;
437         else if (isMouseOver)
438             emissiveOffset = emissiveHovered;
439 
440         foreach(dirtyRect; dirtyRects)
441         {
442             ImageRef!RGBA cDiffuse  = diffuseMap.cropImageRef(dirtyRect);
443             ImageRef!RGBA cMaterial = materialMap.cropImageRef(dirtyRect);
444             ImageRef!L16 cDepth     = depthMap.cropImageRef(dirtyRect);
445 
446             // Basically we'll find a coordinate in the knob image for each pixel in the dirtyRect         
447 
448             enum float renormDepth = 1.0 / 65535.0f;
449             for (int y = 0; y < dirtyRect.height; ++y)
450             {
451 
452 
453                 RGBA* outDiffuse = cDiffuse.scanline(y).ptr;
454                 L16* outDepth = cDepth.scanline(y).ptr;
455                 RGBA* outMaterial = cMaterial.scanline(y).ptr;
456 
457                 if (legacy)
458                 {
459                     // source center 
460                     int W = position.width;
461                     int H = position.height; 
462                     vec2f sourceCenter = vec2f(W*0.5f, H*0.5f);
463 
464                     for (int x = 0; x < dirtyRect.width; ++x)
465                     {
466                         vec2f destPos = vec2f(x + dirtyRect.min.x, y + dirtyRect.min.y);
467                         vec2f sourcePos = sourceCenter + rotate(destPos - center);
468 
469                         // Legacy textures have 
470 
471                         // If the point is outside the knobimage, it is considered to have an alpha of zero
472                         float fAlpha = 0.0f;
473 
474                         if ( (sourcePos.x >= 0.5f) && (sourcePos.x < (H - 0.5f))
475                          &&  (sourcePos.y >=  0.5f) && (sourcePos.y < (H - 0.5f)) )
476                         {
477                             // PERF: sample a single RGBA L16 mipmap once in non-legacy mode
478 
479                             fAlpha = _alphaTexture.linearSample(0, sourcePos.x, sourcePos.y);
480 
481                             if (fAlpha > 0)
482                             {
483                                 ubyte alpha = cast(ubyte)(0.5f + fAlpha / 257.0f);
484 
485                                 if (drawToDiffuse)
486                                 {
487                                     vec4f fDiffuse  =  _diffuseTexture.linearSample(0, sourcePos.x, sourcePos.y);
488                                     ubyte R = cast(ubyte)(0.5f + fDiffuse.r);
489                                     ubyte G = cast(ubyte)(0.5f + fDiffuse.g);
490                                     ubyte B = cast(ubyte)(0.5f + fDiffuse.b);
491                                     int E = cast(ubyte)(0.5f + fDiffuse.a + emissiveOffset);
492                                     if (E < 0) E = 0; 
493                                     if (E > 255) E = 255;
494 
495                                     if (!legacy)
496                                         E = 0;
497 
498                                     // TODO: non-legacy emissive?
499 
500                                     RGBA diffuse = RGBA(R, G, B, cast(ubyte)E);
501                                     outDiffuse[x] = blendColor( diffuse, outDiffuse[x], alpha);
502                                 }
503 
504                                 if (drawToMaterial)
505                                 {
506                                     vec4f fMaterial = _materialTexture.linearSample(0, sourcePos.x, sourcePos.y);
507                                     ubyte Ro = cast(ubyte)(0.5f + fMaterial.r);
508                                     ubyte M = cast(ubyte)(0.5f + fMaterial.g);
509                                     ubyte S = cast(ubyte)(0.5f + fMaterial.b);
510                                     ubyte X = cast(ubyte)(0.5f + fMaterial.a);
511                                     RGBA material = RGBA(Ro, M, S, X);
512                                     outMaterial[x] = blendColor( material, outMaterial[x], alpha);
513                                 }
514 
515                                 if (drawToDepth)
516                                 {
517                                     float fAlphaDepth = fAlpha;
518                                     fAlphaDepth *= 0.00001525902f; // 1 / 65535
519                                     float fDepth    =    _depthTexture.linearSample(0, sourcePos.x, sourcePos.y);
520                                     float interpDepth = fDepth * fAlphaDepth + outDepth[x].l * (1.0f - fAlphaDepth);
521                                     outDepth[x] = L16(cast(ushort)(0.5f + interpDepth) );
522                                 }
523                             }
524                         }
525                     }
526                 }
527                 else
528                 {
529                     version(KnobImage_resizedInternal)
530                     {
531                         int DW = _textureWidth;
532                         int DH = _textureHeight;
533 
534                         vec2f sourceCenter = vec2f(DW*0.5f, DH*0.5f);
535 
536 
537                         // non-legacy drawing builds upon the fact the texture will be alpha-premul
538                         for (int x = 0; x < dirtyRect.width; ++x)
539                         {
540                             vec2f destPos = vec2f(x + dirtyRect.min.x, y + dirtyRect.min.y);
541 
542                             vec2f sourcePos = sourceCenter + rotate(destPos - center) * oversampleTexture;
543 
544 
545                             if ( (sourcePos.x >= 0.5f) && (sourcePos.x < (DW - 0.5f))
546                                     &&  (sourcePos.y >=  0.5f) && (sourcePos.y < (DH - 0.5f)) )
547                             {
548                                 // Get the alpha-premultiplied samples in a single texture.
549 
550                                 if (drawToDiffuse)
551                                 {
552                                     vec4f fDiffuse;
553                                     if (diffuseInterpolation == KnobInterpolationType.linear)
554                                         fDiffuse = _resizedImage.linearSample(0, sourcePos.x, sourcePos.y);
555                                     else
556                                         fDiffuse = _resizedImage.cubicSample(0, sourcePos.x, sourcePos.y);
557                                     if (fDiffuse.a > 0)
558                                     {
559                                         // Convert from 16-bit to 8-bit
560                                         ubyte R = cast(ubyte)(0.5f + fDiffuse.r / 257.0f);
561                                         ubyte G = cast(ubyte)(0.5f + fDiffuse.g / 257.0f);
562                                         ubyte B = cast(ubyte)(0.5f + fDiffuse.b / 257.0f);
563                                         ubyte A = cast(ubyte)(0.5f + fDiffuse.a / 257.0f);
564                                         int E = (emissiveOffset * A) / 255; // Need to premultiply Emissive by Alpha
565                                         RGBA diffuse = RGBA(R, G, B, cast(ubyte)E);
566                                         outDiffuse[x] = blendColorPremul( diffuse, outDiffuse[x], A);
567                                     }
568                                 }
569 
570                                 if (drawToMaterial)
571                                 {
572                                     vec4f fMaterial;
573                                     if (materialInterpolation == KnobInterpolationType.linear)
574                                         fMaterial = _resizedImage.linearSample(0, sourcePos.x + DW*2, sourcePos.y);
575                                     else
576                                         fMaterial = _resizedImage.cubicSample(0, sourcePos.x + DW*2, sourcePos.y);
577 
578                                     if (fMaterial.a > 0)
579                                     {
580                                         // Convert from 16-bit to 8-bit
581                                         ubyte R = cast(ubyte)(0.5f + fMaterial.r / 257.0f);
582                                         ubyte G = cast(ubyte)(0.5f + fMaterial.g / 257.0f);
583                                         ubyte B = cast(ubyte)(0.5f + fMaterial.b / 257.0f);
584                                         ubyte A = cast(ubyte)(0.5f + fMaterial.a / 257.0f);
585                                         RGBA material = RGBA(R, G, B, 0); // clear last channel, means nothing currently
586                                         outMaterial[x] = blendColorPremul( material, outMaterial[x], A);
587                                     }
588                                 }
589 
590                             
591                                 if (drawToDepth)
592                                 {
593                                     vec4f fDepth;
594                                     if (depthInterpolation == KnobInterpolationType.linear)
595                                         fDepth = _resizedImage.linearSample(0, sourcePos.x + DW, sourcePos.y);
596                                     else
597                                         fDepth = _resizedImage.cubicSample(0, sourcePos.x + DW, sourcePos.y);
598 
599                                     if (fDepth.a > 0)
600                                     {
601                                         // Keep it in 16-bit
602                                         float fAlphaDepth = fDepth.a * 0.00001525902f; // 1 / 65535
603                                         float depthHere = outDepth[x].l; //error = 32244
604 
605                                         // As specified, only green is considered as depth value, though you will likely
606                                         // want to raw your depth in grey.
607                                         float interpDepth = fDepth.g + depthHere * (1.0f - fAlphaDepth); // error = 65543.016
608 
609                                         // Note: here it's perfectly possible for interpDepth to overshoot above 65535 because of roundings.
610                                         // if alpha is near 1.0f, and fDepth.g is rounded up, then the result may overshoot.
611                                         // eg: 65532 (ie. white with 0.99957263 alpha) added to (1 - 0.99957263) * 32242.0f
612                                         if (interpDepth > 65535.0f)
613                                         {
614                                             interpDepth = 65535.0f;
615                                         }
616                                         assert(interpDepth >= 0 && interpDepth <= 65535.49);
617                                         outDepth[x] = L16(cast(ushort)(0.5f + interpDepth) );
618                                     }
619                                 }
620                             }
621                         }
622                     }
623                     else
624                     {
625                         // New draw mode for BADAMA_16, interpolates KnobImage directly linearly.
626                         // This is the future.
627 
628                         // Size of input texture.
629                         int DH = _knobImage.image.height;
630                         int DW = DH;
631 
632                         int W = position.width;
633                         int H = position.height;  
634 
635                         vec2f sourceCenter = vec2f(DW*0.5f, DH*0.5f);
636                         vec2f widgetCenter = vec2f(W*0.5f, H*0.5f);
637 
638                         float scaleFactor = DH / cast(float)H;
639 
640 
641                         // non-legacy drawing builds upon the fact the texture will be alpha-premul
642                         for (int x = 0; x < dirtyRect.width; ++x)
643                         {
644                             vec2f destPos = vec2f(x + dirtyRect.min.x, y + dirtyRect.min.y);
645                             vec2f sourcePos = sourceCenter + rotate(destPos - widgetCenter) * scaleFactor;
646 
647                             // Can we sample the texture?
648                             if ( (sourcePos.x >= 0.5f) && (sourcePos.x < (DW - 0.5f))
649                                  &&  (sourcePos.y >=  0.5f) && (sourcePos.y < (DH - 0.5f)) )
650                             {
651                                 // Get the alpha-premultiplied samples in a single texture.
652 
653                                 if (drawToDiffuse)
654                                 {
655                                     vec4f fDiffuse = cubicSampleRGBA16(_knobImage.image, sourcePos.x, sourcePos.y);
656 
657                                     if (fDiffuse.a > 0)
658                                     {
659                                         // Convert from 16-bit to 8-bit
660                                         ubyte R = cast(ubyte)(0.5f + fDiffuse.r / 257.0f);
661                                         ubyte G = cast(ubyte)(0.5f + fDiffuse.g / 257.0f);
662                                         ubyte B = cast(ubyte)(0.5f + fDiffuse.b / 257.0f);
663                                         ubyte A = cast(ubyte)(0.5f + fDiffuse.a / 257.0f);
664                                         int E = (emissiveOffset * A) / 255; // Need to premultiply Emissive by Alpha
665                                         RGBA diffuse = RGBA(R, G, B, cast(ubyte)E);
666                                         outDiffuse[x] = blendColorPremul( diffuse, outDiffuse[x], A);
667                                     }
668                                 }
669 
670                                 if (drawToMaterial)
671                                 {
672                                     vec4f fMaterial;
673                                     fMaterial = cubicSampleRGBA16(_knobImage.image, sourcePos.x + DW*2, sourcePos.y);
674                                     if (fMaterial.a > 0)
675                                     {
676                                         // Convert from 16-bit to 8-bit
677                                         ubyte R = cast(ubyte)(0.5f + fMaterial.r / 257.0f);
678                                         ubyte G = cast(ubyte)(0.5f + fMaterial.g / 257.0f);
679                                         ubyte B = cast(ubyte)(0.5f + fMaterial.b / 257.0f);
680                                         ubyte A = cast(ubyte)(0.5f + fMaterial.a / 257.0f);
681                                         RGBA material = RGBA(R, G, B, 0); // clear last channel, means nothing currently
682                                         outMaterial[x] = blendColorPremul( material, outMaterial[x], A);
683                                     }
684                                 }
685 
686 
687                                 if (drawToDepth)
688                                 {
689                                     vec4f fDepth;
690                                     fDepth = cubicSampleRGBA16(_knobImage.image, sourcePos.x + DW, sourcePos.y);
691 
692                                     if (fDepth.a > 0)
693                                     {
694                                         // Keep it in 16-bit
695                                         float fAlphaDepth = fDepth.a * 0.00001525902f; // 1 / 65535
696                                         float depthHere = outDepth[x].l; //error = 32244
697 
698                                         // As specified, only green is considered as depth value, though you will likely
699                                         // want to raw your depth in grey.
700                                         float interpDepth = fDepth.g + depthHere * (1.0f - fAlphaDepth); // error = 65543.016
701 
702                                         // Note: here it's perfectly possible for interpDepth to overshoot above 65535 because of roundings.
703                                         // if alpha is near 1.0f, and fDepth.g is rounded up, then the result may overshoot.
704                                         // eg: 65532 (ie. white with 0.99957263 alpha) added to (1 - 0.99957263) * 32242.0f
705                                         if (interpDepth > 65535.0f)
706                                         {
707                                             interpDepth = 65535.0f;
708                                         }
709                                         assert(interpDepth >= 0 && interpDepth <= 65535.49);
710                                         outDepth[x] = L16(cast(ushort)(0.5f + interpDepth) );
711                                     }
712                                 }
713                             }
714                         }
715                     }
716                 }
717             }
718         }
719     }
720 
721     KnobImage _knobImage; // borrowed image of the knob
722 
723     // Note: mipmaps are used here, only for the linear sampling ability!
724 
725     // <Only used in BADAMA_16 format>
726     version(KnobImage_resizedInternal)
727     {
728         Mipmap!RGBA16 _resizedImage;
729         int _textureWidth = -1;  // texture size is: 3*_textureWidth, _textureHeight with _textureWidth == _textureHeight being equal.
730         int _textureHeight = -1;
731     }
732     // </Only used in BADAMA_16 format>
733 
734     // <Only used in ABDME_8 format>
735     Mipmap!L16 _alphaTexture;         // owned 1-level image of alpha
736     OwnedImage!L16 _tempBuf; // used for augmenting bitdepth of alpha and depth
737     OwnedImage!RGBA _tempBufRGBA; // used for lessening bitdepth of material and diffuse
738     Mipmap!L16 _depthTexture; // owned 1-level image of depth
739     Mipmap!RGBA _diffuseTexture; // owned 1-level image of diffuse+emissive RGBE
740     Mipmap!RGBA _materialTexture; // owned 1-level image of material
741     // </Only used in ABDME_8 format>
742 
743 }
744 
745 private:
746 
747 // Interpolation in a 1-level mipmap (at level 0), with RGBA16 premultiplied samples.
748 // This is for imageKnob interpolation.
749 vec4f linearSampleRGBA16(ref Image image, float x, float y) nothrow @nogc
750 {
751     assert(image.type() == PixelType.rgba16);
752     alias COLOR = RGBA16;
753 
754     x = x - 0.5f;
755     y = y - 0.5f;
756 
757     if (x < 0)
758         x = 0;
759     if (y < 0)
760         y = 0;
761 
762     __m128 floatCoords = _mm_setr_ps(x, y, 0, 0);
763     __m128i truncatedCoord = _mm_cvttps_epi32(floatCoords);
764     int ix = truncatedCoord.array[0];
765     int iy = truncatedCoord.array[1];
766 
767     // Get fractional part
768     float fx = x - ix;
769     float fy = y - iy;
770 
771     const int maxX = image.width() - 1;
772     const int maxY = image.height() - 1;
773     if (ix > maxX)
774         ix = maxX;
775     if (iy > maxY)
776         iy = maxY;
777 
778     int ixp1 = ix + 1;
779     int iyp1 = iy + 1;
780     if (ixp1 > maxX)
781         ixp1 = maxX;
782     if (iyp1 > maxY)
783         iyp1 = maxY;  
784 
785     float fxm1 = 1 - fx;
786     float fym1 = 1 - fy;
787 
788     COLOR* L0 = cast(COLOR*) image.scanptr(iy);
789     COLOR* L1 = cast(COLOR*) image.scanptr(iyp1);
790 
791     COLOR A = L0[ix];
792     COLOR B = L0[ixp1];
793     COLOR C = L1[ix];
794     COLOR D = L1[ixp1];
795 
796     vec4f vA = vec4f(A.r, A.g, A.b, A.a);
797     vec4f vB = vec4f(B.r, B.g, B.b, B.a);
798     vec4f vC = vec4f(C.r, C.g, C.b, C.a);
799     vec4f vD = vec4f(D.r, D.g, D.b, D.a);
800 
801     vec4f up = vA * fxm1 + vB * fx;
802     vec4f down = vC * fxm1 + vD * fx;
803     vec4f result = up * fym1 + down * fy;
804     return result;
805 }
806 
807 vec4f cubicSampleRGBA16(ref Image image, float x, float y) nothrow @nogc
808 {
809     assert(image.type() == PixelType.rgba16);
810     alias COLOR = RGBA16;
811 
812     x = x - 0.5f;
813     y = y - 0.5f;
814 
815     __m128 mm0123 = _mm_setr_ps(-1, 0, 1, 2);
816     __m128i x_indices = _mm_cvttps_epi32( _mm_set1_ps(x) + mm0123);
817     __m128i y_indices = _mm_cvttps_epi32( _mm_set1_ps(y) + mm0123);
818     __m128i zero = _mm_setzero_si128();
819     x_indices = _mm_max_epi32(x_indices, zero);
820     y_indices = _mm_max_epi32(y_indices, zero);
821     x_indices = _mm_min_epi32(x_indices, _mm_set1_epi32(image.width()-1));
822     y_indices = _mm_min_epi32(y_indices, _mm_set1_epi32(image.height()-1));
823 
824     int i0 = x_indices.array[0];
825     int i1 = x_indices.array[1];
826     int i2 = x_indices.array[2];
827     int i3 = x_indices.array[3];
828 
829     // fractional part
830     float a = x + 1.0f;
831     float b = y + 1.0f;
832     a = a - cast(int)(a);
833     b = b - cast(int)(b);
834     assert(a >= -0.01 && a <= 1.01);
835     assert(b >= -0.01 && b <= 1.01);
836 
837     COLOR*[4] L = void;
838     L[0] = cast(COLOR*) image.scanptr(y_indices.array[0]);
839     L[1] = cast(COLOR*) image.scanptr(y_indices.array[1]);
840     L[2] = cast(COLOR*) image.scanptr(y_indices.array[2]);
841     L[3] = cast(COLOR*) image.scanptr(y_indices.array[3]);
842 
843   
844     {
845         // actually optimized ok by LDC
846         static vec4f clamp_0_to_65535(vec4f a)
847         {
848             if (a[0] < 0) a[0] = 0;
849             if (a[1] < 0) a[1] = 0;
850             if (a[2] < 0) a[2] = 0;
851             if (a[3] < 0) a[3] = 0;
852             if (a[0] > 65535) a[0] = 65535;
853             if (a[1] > 65535) a[1] = 65535;
854             if (a[2] > 65535) a[2] = 65535;
855             if (a[3] > 65535) a[3] = 65535;
856             return a;
857         }
858 
859         static cubicInterp(float t, vec4f x0, vec4f x1, vec4f x2, vec4f x3) pure nothrow @nogc
860         {
861             // PERF: doesn't sound that great???
862             return x1 
863                 + t * ((-0.5f * x0) + (0.5f * x2))
864                 + t * t * (x0 - (2.5f * x1) + (2.0f * x2) - (0.5f * x3))
865                 + t * t * t * ((-0.5f * x0) + (1.5f * x1) - (1.5f * x2) + 0.5f * x3);
866         }
867         vec4f[4] R = void;
868         for (int row = 0; row < 4; ++row)
869         {
870             COLOR* pRow = L[row];
871             COLOR ri0jn = pRow[i0];
872             COLOR ri1jn = pRow[i1];
873             COLOR ri2jn = pRow[i2];
874             COLOR ri3jn = pRow[i3];
875             vec4f A = vec4f(ri0jn.r, ri0jn.g, ri0jn.b, ri0jn.a);
876             vec4f B = vec4f(ri1jn.r, ri1jn.g, ri1jn.b, ri1jn.a);
877             vec4f C = vec4f(ri2jn.r, ri2jn.g, ri2jn.b, ri2jn.a);
878             vec4f D = vec4f(ri3jn.r, ri3jn.g, ri3jn.b, ri3jn.a);
879             R[row] = cubicInterp(a, A, B, C, D);
880         }
881         return clamp_0_to_65535(cubicInterp(b, R[0], R[1], R[2], R[3]));
882     }
883 }