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;
10 
11 import dplug.math.vector;
12 import dplug.math.box;
13 
14 import dplug.core.nogc;
15 import dplug.gui.element;
16 import dplug.pbrwidgets.knob;
17 import dplug.client.params;
18 
19 nothrow:
20 @nogc:
21 
22 
23 /// Type of image being used for Knob graphics.
24 /// It used to be a one level deep Mipmap (ie. a flat image with sampling capabilities).
25 /// It is now a regular `OwnedImage` since it is resized in `reflow()`.
26 /// Use it an opaque type: its definition can change.
27 alias KnobImage = OwnedImage!RGBA;
28 
29 /// Loads a knob image and rearrange channels to be fit to pass to `UIImageKnob`.
30 ///
31 /// The input format of such an image is an an arrangement of squares:
32 ///
33 ///         h              h           h            h         h
34 ///   +------------+------------+------------+------------+-----------+
35 ///   |            |            |            |            |           |
36 ///   |  alpha     |  basecolor |   depth    |  material  |  emissive |
37 /// h |  grayscale |     RGB    |  grayscale |    RMS     | grayscale |
38 ///   |  (R used)  |            |(sum of RGB)|            | (R used)  |
39 ///   |            |            |            |            |           |
40 ///   +------------+------------+------------+------------+-----------+
41 ///
42 ///
43 /// This format is extended so that:
44 /// - the emissive component is copied into the diffuse channel to form a full RGBA quad,
45 /// - same for material with the physical channel, which is assumed to be always "full physical"
46 ///
47 /// Recommended format: PNG, for example a 230x46 24-bit image.
48 /// Note that such an image is formatted and resized in `reflow` before use.
49 ///
50 /// Warning: the returned `KnobImage` should be destroyed by the caller with `destroyFree`.
51 /// Note: internal resizing does not preserve aspect ratio exactly for 
52 ///       approximate scaled rectangles.
53 KnobImage loadKnobImage(in void[] data)
54 {
55     OwnedImage!RGBA image = loadOwnedImage(data);
56 
57     // Expected dimension is 5H x H
58     assert(image.w == image.h * 5);
59 
60     int h = image.h;
61     
62     for (int y = 0; y < h; ++y)
63     {
64         RGBA[] line = image.scanline(y);
65 
66         RGBA[] basecolor = line[h..2*h];
67         RGBA[] material = line[3*h..4*h];
68         RGBA[] emissive = line[4*h..5*h];
69 
70         for (int x = 0; x < h; ++x)
71         {
72             // Put emissive red channel into the alpha channel of base color
73             basecolor[x].a = emissive[x].r;
74 
75             // Fills unused with 255
76             material[x].a = 255;
77         }
78     }
79     return image;
80 }
81 
82 
83 /// UIKnob which replace the knob part by a rotated PBR image.
84 class UIImageKnob : UIKnob
85 {
86 public:
87 nothrow:
88 @nogc:
89 
90     /// If `true`, diffuse data is blended in the diffuse map using alpha information.
91     /// If `false`, diffuse is left untouched.
92     @ScriptProperty bool drawToDiffuse = true;
93 
94     /// If `true`, depth data is blended in the depth map using alpha information.
95     /// If `false`, depth is left untouched.
96     @ScriptProperty bool drawToDepth = true;
97 
98     /// If `true`, material data is blended in the material map using alpha information.
99     /// If `false`, material is left untouched.
100     @ScriptProperty bool drawToMaterial = true;
101 
102     /// Amount of static emissive energy to the Emissive channel.
103     @ScriptProperty ubyte emissive = 0;
104 
105     /// Amount of static emissive energy to add when mouse is over, but not dragging.
106     @ScriptProperty ubyte emissiveHovered = 0;
107 
108     /// Amount of static emissive energy to add when mouse is over, but not dragging.
109     @ScriptProperty ubyte emissiveDragged = 0;
110 
111     /// `knobImage` should have been loaded with `loadKnobImage`.
112     /// Warning: `knobImage` must outlive the knob, it is borrowed.
113     this(UIContext context, KnobImage knobImage, Parameter parameter)
114     {
115         super(context, parameter);
116         _knobImage = knobImage;
117 
118         _tempBuf = mallocNew!(OwnedImage!L16);
119         _alphaTexture = mallocNew!(Mipmap!L16);
120         _depthTexture = mallocNew!(Mipmap!L16);
121         _diffuseTexture = mallocNew!(Mipmap!RGBA);
122         _materialTexture = mallocNew!(Mipmap!RGBA);
123     }
124 
125     ~this()
126     {
127         _tempBuf.destroyFree();
128         _alphaTexture.destroyFree();
129         _depthTexture.destroyFree();
130         _diffuseTexture.destroyFree();
131         _materialTexture.destroyFree();
132     }
133 
134     override void reflow()
135     {
136         int numTiles = 5;
137 
138         // Limitation: the source _knobImage should be multiple of numTiles pixels.
139         assert(_knobImage.w % numTiles == 0);
140         int SH = _knobImage.w / numTiles;
141         assert(_knobImage.h == SH); // Input image dimension should be: (numTiles x SH, SH)
142 
143         // Things are resized towards DW x DH textures.
144         int DW = position.width;
145         int DH = position.height; 
146         //assert(DW == DH); // For now.
147 
148         _tempBuf.size(SH, SH);
149 
150         enum numMipLevels = 1;
151         _alphaTexture.size(numMipLevels, DW, DH);
152         _depthTexture.size(numMipLevels, DW, DH);
153         _diffuseTexture.size(numMipLevels, DW, DH);
154         _materialTexture.size(numMipLevels, DW, DH);
155 
156         auto resizer = context.globalImageResizer;
157 
158         // 1. Extends alpha to 16-bit, resize it to destination size in _alphaTexture.
159         {
160             ImageRef!RGBA srcAlpha =  _knobImage.toRef.cropImageRef(rectangle(0, 0, SH, SH));
161             ImageRef!L16 tempAlpha = _tempBuf.toRef();
162             ImageRef!L16 destAlpha = _alphaTexture.levels[0].toRef;
163             for (int y = 0; y < SH; ++y)
164             {
165                 for (int x = 0; x < SH; ++x)
166                 {
167                     RGBA sample = srcAlpha[x, y];
168                     ushort alpha16 = sample.r * 257;
169                     tempAlpha[x, y] = L16(alpha16);
170                 }
171             }
172             resizer.resizeImageGeneric(tempAlpha, destAlpha);
173         }
174 
175         // 2. Extends depth to 16-bit, resize it to destination size in _depthTexture.
176         {
177             ImageRef!RGBA srcDepth =  _knobImage.toRef.cropImageRef(rectangle(2*SH, 0, SH, SH));
178             ImageRef!L16 tempDepth = _tempBuf.toRef();
179             ImageRef!L16 destDepth = _depthTexture.levels[0].toRef;
180             for (int y = 0; y < SH; ++y)
181             {
182                 for (int x = 0; x < SH; ++x)
183                 {
184                     RGBA sample = srcDepth[x, y];
185                     ushort depth = cast(ushort)(0.5f + (257 * (sample.r + sample.g + sample.b) / 3.0f));
186                     tempDepth[x, y] = L16(depth);
187                 }
188             }
189 
190             // Note: different resampling kernal for depth, to smooth it. 
191             //       Slightly more serene to look at.
192             resizer.resizeImageDepth(tempDepth, destDepth); 
193         }
194 
195         // 3. Resize diffuse+emissive in _diffuseTexture.
196         {
197             ImageRef!RGBA srcDiffuse =  _knobImage.toRef.cropImageRef(rectangle(SH, 0, SH, SH));
198             ImageRef!RGBA destDiffuse = _diffuseTexture.levels[0].toRef;
199             resizer.resizeImageDiffuse(srcDiffuse, destDiffuse);
200         }
201 
202         // 4. Resize material in _materialTexture.
203         {
204             ImageRef!RGBA srcMaterial =  _knobImage.toRef.cropImageRef(rectangle(3*SH, 0, SH, SH));
205             ImageRef!RGBA destMaterial = _materialTexture.levels[0].toRef;
206             resizer.resizeImageMaterial(srcMaterial, destMaterial);
207         }
208     }
209 
210 
211     override void drawKnob(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects)
212     {
213         float radius = getRadius();
214         vec2f center = getCenter();
215         float valueAngle = getValueAngle() + PI_2;
216         float cosa = cos(valueAngle);
217         float sina = sin(valueAngle);
218 
219         int w = _alphaTexture.width;
220         int h = _alphaTexture.height;
221 
222         // Note: slightly incorrect, since our resize in reflow doesn't exactly preserve aspect-ratio
223         vec2f rotate(vec2f v) pure nothrow @nogc
224         {
225             return vec2f(v.x * cosa + v.y * sina, 
226                          v.y * cosa - v.x * sina);
227         }
228 
229         int emissiveOffset = emissive;
230         if (isDragged)
231             emissiveOffset = emissiveDragged;
232         else if (isMouseOver)
233             emissiveOffset = emissiveHovered;
234 
235         foreach(dirtyRect; dirtyRects)
236         {
237             ImageRef!RGBA cDiffuse  = diffuseMap.cropImageRef(dirtyRect);
238             ImageRef!RGBA cMaterial = materialMap.cropImageRef(dirtyRect);
239             ImageRef!L16 cDepth     = depthMap.cropImageRef(dirtyRect);
240 
241             // Basically we'll find a coordinate in the knob image for each pixel in the dirtyRect 
242 
243             // source center 
244             vec2f sourceCenter = vec2f(w*0.5f, h*0.5f);
245 
246             enum float renormDepth = 1.0 / 65535.0f;
247             for (int y = 0; y < dirtyRect.height; ++y)
248             {
249                 RGBA* outDiffuse = cDiffuse.scanline(y).ptr;
250                 L16* outDepth = cDepth.scanline(y).ptr;
251                 RGBA* outMaterial = cMaterial.scanline(y).ptr;
252 
253                 for (int x = 0; x < dirtyRect.width; ++x)
254                 {
255                     vec2f destPos = vec2f(x + dirtyRect.min.x, y + dirtyRect.min.y);
256                     vec2f sourcePos = sourceCenter + rotate(destPos - center);
257 
258                     // If the point is outside the knobimage, it is considered to have an alpha of zero
259                     float fAlpha = 0.0f;
260                     if ( (sourcePos.x >= 0.5f) && (sourcePos.x < (h - 0.5f))
261                      &&  (sourcePos.y >=  0.5f) && (sourcePos.y < (h - 0.5f)) )
262                     {
263                         fAlpha = _alphaTexture.linearSample(0, sourcePos.x, sourcePos.y);
264 
265                         if (fAlpha > 0)
266                         {
267                             ubyte alpha = cast(ubyte)(0.5f + fAlpha / 257.0f);
268 
269                             if (drawToDiffuse)
270                             {
271                                 vec4f fDiffuse  =  _diffuseTexture.linearSample(0, sourcePos.x, sourcePos.y);
272                                 ubyte R = cast(ubyte)(0.5f + fDiffuse.r);
273                                 ubyte G = cast(ubyte)(0.5f + fDiffuse.g);
274                                 ubyte B = cast(ubyte)(0.5f + fDiffuse.b);
275                                 int E = cast(ubyte)(0.5f + fDiffuse.a + emissiveOffset);
276                                 if (E < 0) E = 0; 
277                                 if (E > 255) E = 255;
278                                 RGBA diffuse = RGBA(R, G, B, cast(ubyte)E);
279                                 outDiffuse[x] = blendColor( diffuse, outDiffuse[x], alpha);
280                             }
281 
282                             if (drawToMaterial)
283                             {
284                                 vec4f fMaterial = _materialTexture.linearSample(0, sourcePos.x, sourcePos.y);
285                                 ubyte Ro = cast(ubyte)(0.5f + fMaterial.r);
286                                 ubyte M = cast(ubyte)(0.5f + fMaterial.g);
287                                 ubyte S = cast(ubyte)(0.5f + fMaterial.b);
288                                 ubyte X = cast(ubyte)(0.5f + fMaterial.a);
289                                 RGBA material = RGBA(Ro, M, S, X);
290                                 outMaterial[x] = blendColor( material, outMaterial[x], alpha);
291                             }
292 
293                             if (drawToDepth)
294                             {
295                                 float fDepth    =    _depthTexture.linearSample(0, sourcePos.x, sourcePos.y);
296                                 ushort depth = cast(ushort)(0.5f + fDepth);
297                                 int interpolatedDepth = depth * alpha + outDepth[x].l * (255 - alpha);
298                                 outDepth[x] = L16(cast(ushort)( (interpolatedDepth + 128) / 255));
299                             }
300                         }
301                     }
302                 }
303             }
304         }
305     }
306 
307     KnobImage _knobImage; // borrowed image of the knob
308 
309     OwnedImage!L16 _tempBuf; // used for augmenting bitdepth of alpha and depth
310     Mipmap!L16 _alphaTexture; // owned 1-level image of alpha
311     Mipmap!L16 _depthTexture; // owned 1-level image of depth
312     Mipmap!RGBA _diffuseTexture; // owned 1-level image of diffuse+emissive RGBE
313     Mipmap!RGBA _materialTexture; // owned 1-level image of material
314 }
315