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 }