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