diff --git a/include/vtfpp/DistanceMapping.h b/include/vtfpp/DistanceMapping.h index 92199a0b1..ffde99a23 100644 --- a/include/vtfpp/DistanceMapping.h +++ b/include/vtfpp/DistanceMapping.h @@ -15,9 +15,10 @@ enum class Dither { enum class Flags : uint32_t { NONE = 0, - DISTANCEAA = 1 << 0, ///< Experimental; interpret the alpha channel as antialiased. Can result in a more precise distance map, but produces nonsense if large gradients are present. - EUCLIDEAN = 1 << 1, ///< The distance-mapping algorithm is a brute-force scan of a *square* area. If this is enabled, only accept distance hits in a circular area. + DISTANCEAA = 1 << 0, ///< Experimental; interpret the alpha channel as antialiased. Can result in a more precise distance map, but can produce nonsense at sharp corners. + EUCLIDEAN = 1 << 1, ///< Meaningless. Preserved for backward compatibility. Never actually did anything. SAMPLECENTERED = 1 << 2, ///< Search from the center of pixels (in destination coordinate space) rather than in their north-west corners. Can mitigate a perceived southeast shift at extreme reductions. + }; SOURCEPP_BITFLAGS_ENUM(Flags) diff --git a/src/vtfpp/DistanceMapping.cpp b/src/vtfpp/DistanceMapping.cpp index 04a0a1f89..8d0c048c2 100644 --- a/src/vtfpp/DistanceMapping.cpp +++ b/src/vtfpp/DistanceMapping.cpp @@ -16,51 +16,46 @@ using namespace ImageConversion; using math::Vec2i; using math::Vec2f32; -enum Leaf : uint8_t { - IN, // leaf is opaque - OUT, // leaf is transparent - FIGUREITOUT, // leaf is mixed but small so you should brute force scan. -}; - -uint16_t i32tou16sat(int32_t i, int32_t cap = UINT16_MAX) { - return static_cast(std::clamp(i, 0, cap)); -} - uint16_t ftoabsu16(float f) { - return i32tou16sat(static_cast(fabs(f))); + return static_cast(std::clamp(static_cast(fabs(f)), 0, UINT16_MAX)); } -using IndexFunction = std::function; - -int32_t indexClamped(Vec2i v, uint16_t w, uint16_t h, uint8_t pxLen, uint8_t alphaOffs) { - int32_t rX = i32tou16sat(v[0], w - 1); - int32_t rY = i32tou16sat(v[1], h - 1); - return (rY * w + rX) * pxLen + alphaOffs; +// literally just stb. grep '_full( int n' in stb_image_resize2.h +int32_t edgeClamp(int32_t n, int32_t max) { + if (n < 0) { + return 0; + } + return (n >= max) ? max - 1 : n; } - -int32_t indexReflected(Vec2i v, uint16_t w, uint16_t h, uint8_t pxLen, uint8_t alphaOffs) { - int32_t rX = v[0] >= w ? w - v[0] % w : v[0]; - int32_t rY = v[1] >= h ? h - v[1] % h : v[1]; - return (rY * w + rX) * pxLen + alphaOffs; +int32_t edgeReflect(int32_t n, int32_t max) { + if (n < 0) { + return n > -max ? -n : max - 1; + } + if (n >= max) { + const int32_t max2 = 2 * max; + return n >= max2 ? 0 : max2 - n - 1; + } + return n; } - -int32_t indexWrapped(Vec2i v, uint16_t w, uint16_t h, uint8_t pxLen, uint8_t alphaOffs) { - int32_t rX = (v[0] + w) % w; - int32_t rY = (v[1] + h) % h; - return (rY * w + rX) * pxLen + alphaOffs; +int32_t edgeWrap(int32_t n, int32_t max) { + if (n >= 0) { + return n % max; + } + const int32_t m = (-n) % max; + return m ? max - m : 0; } -IndexFunction indexBy(ImageConversion::ResizeEdge edge) +int32_t indexWith(ImageConversion::ResizeEdge edge, Vec2i v, uint16_t w, uint16_t h, uint8_t pxLen, uint8_t alphaOffs ) { switch (edge) { default: break; case ImageConversion::ResizeEdge::REFLECT: - return indexReflected; + return (edgeReflect(v[1], h) * w + edgeReflect(v[0], w)) * pxLen + alphaOffs; case ImageConversion::ResizeEdge::WRAP: - return indexWrapped; + return (edgeWrap(v[1], h) * w + edgeWrap(v[0], w)) * pxLen + alphaOffs; } - return indexClamped; + return (edgeClamp(v[1], h) * w + edgeClamp(v[0], w)) * pxLen + alphaOffs; } // big ugly immutable record of everything pertaining to how we read alpha from an image @@ -87,174 +82,131 @@ class SampleParam { , reduceX(reduceX) , reduceY(reduceY) , searchRadius(ftoabsu16(2.0f * std::max(reduceX, reduceY) * distanceSpread)) - // the second power of two after search radius - , granularity(uint16_t{4} << ftoabsu16(ceil(log2(static_cast(this->searchRadius))))) , offsX(sampleCentered ? reduceX / 2 : 0) , offsY(sampleCentered ? reduceY / 2 : 0) , alphaThreshold(alphaThreshold) - , indexFn(indexBy(edge)) + , edge(edge) {} - float sample(Vec2i v) const { - return this->srcImg[this->indexFn(v, this->imgWidth, this->imgHeight, this->pxLen, this->alphaOffs)]; + // n.b.: "padded" by search radius so the distance transform can read past + // the edges in accordance with the edge policy + float samplePadded(Vec2i v) const { + return this->srcImg[indexWith(this->edge, v - this->searchRadius, this->imgWidth, this->imgHeight, this->pxLen, this->alphaOffs)]; } - Leaf thresh(float alpha) const { - return alpha >= this->alphaThreshold ? IN : OUT; + bool isInPadded(Vec2i v) const { + return this->samplePadded(v) >= this->alphaThreshold; } const float *const srcImg; const float alphaThreshold; const uint16_t imgHeight; const uint16_t imgWidth; + const uint16_t reduceX, reduceY; const uint16_t searchRadius; - const uint16_t granularity; const uint16_t offsX, offsY; - const uint16_t reduceX, reduceY; const uint8_t pxLen; const uint8_t alphaOffs; - const IndexFunction indexFn; + const ImageConversion::ResizeEdge edge; }; -// immutable map detailing where we can skip computation -struct QuadTree { - QuadTree(const SampleParam *param, uint32_t &totalComputation) : root(this->scanImg(0, 0, param->imgWidth, param->imgHeight, param, totalComputation)) - {} - ~QuadTree() { - Node::wither(root); - } - - struct Node; - using Branch = std::variant; - struct Node { - const std::array branches; - Node(Branch nw, Branch ne, Branch sw, Branch se) : branches {nw, ne, sw, se} {}; - ~Node() { - for ( Branch branch : this->branches ) { - wither(branch); - } - } - static void wither(Branch c) { - if (holds_alternative(c)) { - delete get(c); - } - } - }; +float vecDist(Vec2f32 v0, Vec2f32 v1) { + return std::hypot(v0[0] - v1[0], v0[1] - v1[1]); +} - static bool brancheq(Branch a, Branch b) { - return holds_alternative(a) && holds_alternative(b) && get(a) == get(b); - } +using math::pi_f32; - const Branch root; -private: - // TODO: cleverer scan to partition top-down? - static Branch scanImg(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const SampleParam *param, uint32_t &totalComputation) { - if (w <= param->granularity || h <= param->granularity) { - // extend sensitivity to minimum safe distance. a safe leaf is a safe leaf, no need to peek. - int32_t ix = x - param->searchRadius + param->offsX, tx = x + w + param->searchRadius + param->offsX; - int32_t iy = y - param->searchRadius + param->offsY, ty = y + h + param->searchRadius + param->offsY; - Leaf curShade = param->thresh(param->sample({ix, iy})); - - for (auto jx = ix; jx < tx; jx++) { - for (auto jy = iy; jy < ty; jy++) { - if (param->thresh(param->sample({jx, jy})) != curShade) { - totalComputation += w / param->reduceX * h / param->reduceY; - return FIGUREITOUT; - } - } - } +static constexpr float BIG_EPSILON = 0.05f; +static constexpr float SMALL_EPSILON = 1e-6f; - return curShade; - } +// scuffed at sharp angled points but uh. horseshoes, government work, etc +float aaOffsetPadded(const SampleParam *param, Vec2i v, bool outside) { + const float alpha = param->samplePadded(v); + const float reach = outside ? 1.0f : 0.5f; + float slope = 0.0f; + for (Vec2i offset : {Vec2i{-1, -1}, {0, -1}, {1, -1}, {-1, 0}, {1, 0}, {-1, 1}, {0, 1}, {1, 1}}) { + slope = fmax(slope, fabs(param->samplePadded(v + offset) - alpha) / std::hypot(offset[0], offset[1])); + } + const float gap = fabs(alpha - 0.5f); + if (slope < SMALL_EPSILON || gap > reach * slope) { + return outside ? 0.0f : -1.0f; + } + return fmin(gap / slope, reach); +} - uint16_t hw = w / 2, hh = h / 2; - Branch iNW = scanImg(x, y, hw, hh, param, totalComputation); - Branch iNE = scanImg(x + hw, y, hw, hh, param, totalComputation); - Branch iSW = scanImg(x, y + hh, hw, hh, param, totalComputation); - Branch iSE = scanImg(x + hw, y + hh, hw, hh, param, totalComputation); +template +T sq(T f) { + return f * f; +} - if (brancheq(iNW, iNE) && brancheq(iNE, iSW) && brancheq(iSW, iSE)) { - return get(iNW); - } - return new Node(iNW, iNE, iSW, iSE); +static constexpr float inff = std::numeric_limits::infinity(); +static constexpr double infd = std::numeric_limits::infinity(); + +// Distance Transforms of Sampled Functions +// Pedro F. Felzenszwalb, Daniel P. Huttenlocher. +// https://cs.brown.edu/people/pfelzens/papers/dt-final.pdf +// +// mostly just the 1D transform on page 6 (420) but: +// - optionally report argmin of the envelope bc aa/dither need pixel positions +// - this still isn't perfect information on its own, but *is* enough to +// determine whether whipping out the ol raster scan is necessary +// - cut off outside a particular window +void distanceTransform1D( + const float *f, + int32_t n, + float *d, + int32_t *argmin, + int32_t window, + int32_t *v, + double *z, + int32_t first, + int32_t stride +) { + int32_t q0 = 0; + while (q0 < n && std::isinf(f[q0])) { + q0++; } -}; - -// traverse a QuadTree at one row of a leaf at a time -class RasterCursor { -public: - RasterCursor(const QuadTree *tree, uint16_t imgWidth, uint16_t imgHeight, uint16_t reduceX = 1, uint16_t reduceY = 1, uint16_t offsX = 0, uint16_t offsY = 0) - : tree(tree) - , imgWidth(imgWidth) - , imgHeight(imgHeight) - , reduceX(reduceX) - , reduceY(reduceY) - , offsX(offsX) - , curX(offsX) - , curY(offsY) - { - this->curShade = this->reorient(); + if (q0 == n) { + const int32_t m = (n - first + stride - 1) / stride; + std::fill(d, d + m, inff); + if (argmin) { + std::fill(argmin, argmin + m, -1); + } + return; } - RasterCursor(const QuadTree *tree, const SampleParam *param) - : RasterCursor(tree, param->imgWidth, param->imgHeight, param->reduceX, param->reduceY, param->offsX, param->offsY) - {} - bool getContiguousRun(Leaf &srcShade, uint16_t &srcY, uint16_t &srcX, uint16_t &dstW) { - srcX = this->curX; - srcY = this->curY; - dstW = 0; - srcShade = this->curShade = this->reorient(); - - while (this->curX < this->imgWidth && QuadTree::brancheq(srcShade, this->curShade)) { - auto blkSize = this->imgWidth >> this->depth; - this->curX += blkSize; - dstW += blkSize / this->reduceX; - this->curShade = this->reorient(); + int32_t k = 0; + v[0] = q0; + z[0] = -infd; + z[1] = +infd; + for (int32_t q = q0 + 1; q < n; q++) { + if (std::isinf(f[q])) { + continue; } - - if (this->curX >= this->imgWidth) { - this->curX = this->offsX; - this->curY += this->reduceY; + six: // i love academic pseudo algol very cool + const double s = ((f[q] + sq(q)) - (f[v[k]] + sq(v[k]))) / (2.0 * (q - v[k])); + if (s <= z[k]) { + k--; + goto six; } - - return this->curY < this->imgHeight; + k++; + v[k] = q; + z[k] = s; + z[k + 1] = infd; } -private: - Leaf reorient() { - this->depth = 0; - this->curBranch = this->tree->root; - auto remX = this->curX, remY = this->curY; - - while (std::holds_alternative(this->curBranch)) { - auto benchX = this->imgWidth >> (this->depth + 1), benchY = this->imgHeight >> (this->depth + 1); - if (benchX < 2 || benchY < 2) { - return this->curShade; // would be nice to make this unrepresentable - } - auto bGtX = remX >= benchX, bGtY = remY >= benchY; - remX -= benchX * bGtX; - remY -= benchY * bGtY; - this->curBranch = std::get(this->curBranch)->branches[bGtX | bGtY << 1]; - this->depth++; - } - return get(this->curBranch); + k = 0; + for (int32_t q = first, j = 0; q < n; q += stride, j++) { + while (z[k + 1] < q) { + k++; + } + const bool out = v[k] < q - window || v[k] > q + window - 1; + d[j] = out ? inff : f[v[k]] + sq(q - v[k]); + if (argmin) { + argmin[j] = out ? -1 : v[k]; + } } - - const QuadTree *tree; - uint16_t curX, curY, depth; - const uint16_t imgWidth, imgHeight, reduceX, reduceY, offsX; - Leaf curShade; - QuadTree::Branch curBranch; -}; - -float vecDist(Vec2f32 v0, Vec2f32 v1) { - return std::hypot(v0[0] - v1[0], v1[0] - v1[1]); } -Vec2i vec2ipmul(Vec2i v0, Vec2i v1) { - return {v0[0] * v1[0], v0[1] * v1[1]}; -} - -using math::pi_f32; void paintMap( const SampleParam *param, @@ -262,84 +214,176 @@ void paintMap( uint8_t dstPxLen, uint8_t dstAlphaOffs, bool distanceAA, - bool euclidean, const float *tangentDiffusion, bool *valveQuirks ) { - uint32_t totalComputation = 0; - - const QuadTree tree(param, totalComputation); - RasterCursor cursor(&tree, param); - - uint32_t iOut = dstAlphaOffs; - Leaf srcShade = FIGUREITOUT; - uint16_t srcY = 0, srcX = 0, dstW = 0; + const auto [padW, padH] = (math::Vec2ui{param->imgWidth, param->imgHeight} + 2 * param->searchRadius).values; + std::vector mask(padW * padH); // vector was slower dont @ me + for (uint32_t padY = 0; padY < padH; padY++) { + for (uint32_t padX = 0; padX < padW; padX++) { + mask[padX * padH + padY] = param->isInPadded(Vec2i{padX, padY}); + } + } float fRadius = static_cast(param->searchRadius); uint16_t dstWidth = param->imgWidth / param->reduceX; uint16_t dstHeight = param->imgHeight / param->reduceY; - uint32_t iGradW = 0; - std::vector gradients = tangentDiffusion ? std::vector(totalComputation, -pi_f32 - 0.05f) : std::vector(0); - float lastAng = 0; - float hypot1 = 1; - - bool keepPainting = true; - do { - keepPainting = cursor.getContiguousRun(srcShade, srcY, srcX, dstW); - float fillDistance = 1.0f; - switch (srcShade) { - case OUT: - fillDistance = 0.0f; - case IN: - for (uint16_t i = 0; i < dstW; i++, iOut += dstPxLen) { - dstImg[iOut] = fillDistance; + std::vector gradients(tangentDiffusion ? static_cast(dstWidth) * dstHeight : 0, -pi_f32 - BIG_EPSILON); + + const bool needArgmin = tangentDiffusion || distanceAA; + const size_t padMax = std::max(padW, padH); + std::vector df2D(padW * dstHeight); + std::vector df1D(padMax); + std::vector argmin2D(needArgmin ? padW * dstHeight : 0); + std::vector argmin1D(padMax); + std::vector col(padH); + std::vector seedTally(needArgmin ? padW + 1 : 0); + std::vector v(padMax); + std::vector z(padMax + 1); + + for (uint8_t seed : {1, 0}) { + for (uint32_t padX = 0; padX < padW; padX++) { + for (uint32_t padY = 0; padY < padH; padY++) { + col[padY] = mask[padX * padH + padY] == seed ? 0.0f : inff; } - break; - case FIGUREITOUT: - default: - // oh no we actually have to do work - for (uint16_t i = 0; i < dstW; i++, iGradW++, iOut += dstPxLen) { - int32_t curX = srcX + i * param->reduceX; - float alphaRef = param->sample({curX, srcY}); - Leaf stateRef = param->thresh(alphaRef); - float nearest = std::numeric_limits::max(); - for (int32_t sX = curX - param->searchRadius, tX = curX + param->searchRadius; sX < tX; sX++) { - for (int32_t sY = srcY - param->searchRadius, tY = srcY + param->searchRadius; sY < tY; sY++) { - float alphaSmp = param->sample({sX, sY}); - Leaf stateSmp = param->thresh(alphaSmp); - if (stateSmp != stateRef) { - float dist = std::hypot(sX - curX, sY - srcY); - - if (tangentDiffusion || distanceAA || euclidean) { - lastAng = atan2(sX - curX, sY - srcY); - } - if (distanceAA || euclidean) { - hypot1 = 1 / cos(fabs(fmod(lastAng + pi_f32 * 2.25f, pi_f32 / 2.0f) - pi_f32 / 4.0f)); - } - if (distanceAA) { - dist += hypot1 * (1 - fabs(alphaSmp - alphaRef)); - } - if (euclidean && dist > fRadius + hypot1) { + distanceTransform1D( + col.data(), padH, df1D.data(), + needArgmin ? argmin1D.data() : nullptr, + param->searchRadius, + v.data(), z.data(), + param->offsY + param->searchRadius, param->reduceY + ); + for (uint32_t dstY = 0; dstY < dstHeight; dstY++) { + df2D[dstY * padW + padX] = df1D[dstY]; + if (needArgmin) { + argmin2D[dstY * padW + padX] = argmin1D[dstY]; + } + } + } + + for (uint32_t dstY = 0; dstY < dstHeight; dstY++) { + const int32_t padY = dstY * param->reduceY + param->offsY + param->searchRadius; + distanceTransform1D( + df2D.data() + dstY * padW, padW, df1D.data(), + needArgmin ? argmin1D.data() : nullptr, + padW, + v.data(), z.data(), + param->offsX + param->searchRadius, param->reduceX + ); + + // running count for quick ez checks below; cf. seedInWindow + if (needArgmin) { + seedTally[0] = 0; + for (uint32_t c = 0; c < padW; c++) { + seedTally[c + 1] = seedTally[c] + !std::isinf(df2D[dstY * padW + c]); + } + } + for (uint32_t dstX = 0; dstX < dstWidth; dstX++) { + const int32_t padX = dstX * param->reduceX + param->offsX + param->searchRadius; + if (mask[padX * padH + padY] == seed) { + continue; + } + const int32_t windowStart = padX - param->searchRadius; + const int32_t windowEnd = padX + param->searchRadius; + const bool seedInWindow = needArgmin && seedTally[windowEnd] > seedTally[windowStart]; + float nearest = sqrtf(df1D[dstX]); + int32_t nearestX = seedInWindow ? argmin1D[dstX] : -1; + if (seedInWindow && nearest >= fRadius) { + float nearest2 = std::numeric_limits::max(); + int32_t windowCol = -1; + for (int32_t c = windowStart; c < windowEnd; c++) { + if (const float dist2 = df2D[dstY * padW + c] + sq(padX - c); dist2 < nearest2) { + nearest2 = dist2; + windowCol = c; + } + } + if (windowCol >= 0) { + nearest = sqrtf(nearest2); + nearestX = windowCol; + } + } + int32_t nearestY = -1; + if (distanceAA) { + if (seedInWindow) { + const float outer = nearest + 1; + const float outer2 = sq(outer); + const float inner2 = sq(nearest) - 1; + const int32_t loX = -std::min(outer, param->searchRadius); + const int32_t hiX = +std::min(outer + 1, param->searchRadius); + const float r2 = sq(param->searchRadius); + float preciseNearest = inff; + + for (int32_t dX = loX; dX < hiX; dX++) { + const float dX2 = sq(dX); + const float remHi = outer2 - dX2; + if (remHi < 0.0f || dX2 + r2 < inner2) { continue; } - nearest = fmin(nearest, dist); + const int32_t hi = sqrtf(remHi); + const int32_t lo = inner2 > dX2 ? ceilf(sqrtf(inner2 - dX2)) : 0; + const int32_t aaX = padX + dX; + const int32_t loY = -std::min(hi, param->searchRadius); + const int32_t hiY = +std::min(hi + 1, param->searchRadius); + for (int32_t dY = loY; dY < hiY; dY++) { + if (-lo < dY && dY < lo) { + dY = lo - 1; + continue; + } + const int32_t aaY = padY + dY; + if (mask[aaX * padH + aaY] != seed) { + continue; + } + // it would maybe make more "sense" to just say + // aa means you should set threshold 0.5, but + // threshold is useful to keep independent, + // because image editors are stupid and love + // discarding the rgb values of transparent + // pixels. even tgas. and as long as that stays + // the case, we want to enable the trick of + // exporting with a background at 1/255 alpha + // to force the image to retain rgb there. + // btw if you didn't know that trick and this + // is how you find out, you're welcome :^) + float offsSeed = aaOffsetPadded(param, {aaX, aaY}, true); + if (seed && param->samplePadded({aaX, aaY}) < 0.5f) { + offsSeed = -offsSeed; + } + const float preciseDist = static_cast(std::hypot(dX, dY)) - offsSeed; + if (preciseDist < preciseNearest) { + preciseNearest = preciseDist; + nearestX = aaX; + nearestY = aaY; + } + } + } + if (preciseNearest < inff) { + nearest = preciseNearest; } } + const float offsRef = aaOffsetPadded(param, {padX, padY}, false); + if (offsRef >= 0.0f) { + nearest = fmin(nearest, offsRef); + } } + if (nearestX >= 0 && nearestY < 0) { + nearestY = argmin2D[dstY * padW + nearestX]; + } + nearest = fmax(nearest, 0.0f); nearest = fmin(0.5f, nearest * 0.5f / fRadius); - if (stateRef == OUT) { + if (seed) { nearest = -nearest; } - dstImg[iOut] = 0.5 + nearest; + const uint32_t iDst = dstY * dstWidth + dstX; + dstImg[iDst * dstPxLen + dstAlphaOffs] = 0.5 + nearest; if (tangentDiffusion) { - gradients[iGradW] = lastAng; + gradients[iDst] = nearestX >= 0 ? atan2(nearestY - padY, nearestX - padX) : (-pi_f32 - BIG_EPSILON); } } } - } while (keepPainting); + } if (tangentDiffusion) { // attempt to diffuse quantization error tangent to the gradient of the @@ -373,12 +417,12 @@ void paintMap( // ⎣ SW+NE S+N SE+NW ⎦, // // yielding our per-destination-pixel kernel k. a neat property of this - // is that we can continue ignoring pixels we previously ignored thanks - // to the quadtree (and thus, never set any angle for): we're diffusing - // error tangent to the gradient, and that way lie only pixels where we - // also already set an angle. we can index angles independently, which, - // depending on the contents of the source image, might require a small - // fraction of the space compared to the distance map (or just as much) + // is that we can ignore pixels that aren't within radius of one of the + // parabolas (and thus, never set any angle for): we're diffusing error + // tangent to the gradient, and that way lie only pixels where we would + // also find a parabola. we can't index angles *independently* anymore, + // but oh well who cares. at least we can still commit to this terrible + // bit of torturing every single line of this explanation to 68 columns static constexpr std::array hardSigns{0, 1, 1, 1, 0, -1, -1, -1}; // of course, in our case, y grows downward, so we really want is E..NW // which, conveniently enough, is just 0..3, so no aux lookup table. if @@ -387,35 +431,25 @@ void paintMap( // preceding calculations can run in our flipped coordinates just fine. static constexpr float eighth = pi_f32 / 4.0f; - RasterCursor diffCursor(&tree, dstWidth, dstHeight); - uint32_t diffOut = dstAlphaOffs; - Leaf diffShade = FIGUREITOUT; - const uint32_t dstBufLen = dstWidth * dstHeight * dstPxLen; - uint16_t diffY = 0, diffX = 0, diffW = 0; - uint32_t iGradR = 0; - - bool keepDiffusing = true; - do { - keepDiffusing = diffCursor.getContiguousRun(diffShade, diffY, diffX, diffW); - if (diffShade != FIGUREITOUT) { - continue; - } - bool rightEdge = diffX + diffW >= dstWidth; - - for (uint16_t i = 0; i < diffW; i++, iGradR++, diffOut += dstPxLen) { - float theta = gradients[iGradR]; - if (theta < -pi_f32 - 0.025f) { + const double quantum = *tangentDiffusion; + for (uint32_t diffY = 0; diffY < dstHeight; diffY++) { + for (uint32_t diffX = 0; diffX < dstWidth; diffX++) { + const float theta = gradients[diffY * dstWidth + diffX]; + if (theta < -pi_f32 - BIG_EPSILON / 2.0f) { continue; } float thetaNormal = fmod(theta + 2.5f * pi_f32, 2.0f * pi_f32); uint8_t nEighths = static_cast(std::floor(thetaNormal / eighth)) % 8; - double quantum = *tangentDiffusion; - double curPx = dstImg[diffOut]; + Vec2i pos{diffX, diffY}; + uint32_t iDiff = indexWith(param->edge, pos, dstWidth, dstHeight, dstPxLen, dstAlphaOffs); + double curPx = dstImg[iDiff]; + double quantized = std::round(curPx / quantum) * quantum; float quantErr = curPx - quantized; - dstImg[diffOut] = quantized; - bool atRight = rightEdge && i >= diffW - 1; - bool bottomEdge = diffY >= dstHeight - 1; + dstImg[iDiff] = quantized; + bool atRight = diffX + 1 >= dstWidth; + bool bottomEdge = pos[1] >= dstHeight - 1; + if (atRight && bottomEdge) { break; } @@ -424,17 +458,13 @@ void paintMap( float dist0 = vecDist(normal, {hardSigns[(nEighths + 2) % 8], hardSigns[(nEighths + 0) % 8]}); float dist1 = vecDist(normal, {hardSigns[(nEighths + 3) % 8], hardSigns[(nEighths + 1) % 8]}); // offset selection uses the part of the circle that is yet to be scanned - Vec2i offs0{hardSigns[nEighths % 4 + 2], hardSigns[nEighths % 4 + 0]}; - Vec2i offs1{hardSigns[nEighths % 4 + 3], hardSigns[nEighths % 4 + 1]}; - Vec2i pos{diffX + i, diffY}; - if (bottomEdge) { - offs0 = vec2ipmul(offs0, {1, 0}); - offs1 = vec2ipmul(offs1, {1, 0}); - } - dstImg[param->indexFn(pos + offs0, dstWidth, dstHeight, dstPxLen, dstAlphaOffs)] += quantErr * dist1 / (dist0 + dist1); - dstImg[param->indexFn(pos + offs1, dstWidth, dstHeight, dstPxLen, dstAlphaOffs)] += quantErr * dist0 / (dist0 + dist1); + Vec2i offs0{hardSigns[(nEighths + 0) % 4 + 2], bottomEdge ? 0 : hardSigns[(nEighths + 0) % 4]}; + Vec2i offs1{hardSigns[(nEighths + 1) % 4 + 2], bottomEdge ? 0 : hardSigns[(nEighths + 1) % 4]}; + + dstImg[indexWith(param->edge, pos + offs0, dstWidth, dstHeight, dstPxLen, dstAlphaOffs)] += quantErr * dist1 / (dist0 + dist1); + dstImg[indexWith(param->edge, pos + offs1, dstWidth, dstHeight, dstPxLen, dstAlphaOffs)] += quantErr * dist0 / (dist0 + dist1); } - } while (keepDiffusing); + } } if (valveQuirks) { @@ -442,7 +472,7 @@ void paintMap( auto blackOutAndWarn = [&](auto oi) { float& px = dstImg[oi * dstPxLen + dstAlphaOffs]; - if (fabs(px) > 0.000001f) { + if (fabs(px) > SMALL_EPSILON) { *valveQuirks = true; } px = 0.0f; @@ -516,7 +546,8 @@ bool isSingleChannel(ImageFormat format) { : std::vector(sizeof(float) * dstPxLen * dstWidth * dstHeight, std::byte{0x00}); - float quantum = 1.0f / static_cast(1 << decompressedAlpha(outFormat)); + int8_t alphaBits = decompressedAlpha(outFormat); + float quantum = (alphaBits > 0) ? 1.0f / static_cast((1 << alphaBits) - 1) : 1.0f; bool doGradientDither = dither == Dither::GRADIENT_TANGENT && !decimal(outFormat); paintMap( ¶m, @@ -524,7 +555,6 @@ bool isSingleChannel(ImageFormat format) { dstPxLen, dstAlphaOffs, !!(flags & Flags::DISTANCEAA), - !!(flags & Flags::EUCLIDEAN), doGradientDither ? &quantum : nullptr, valveQuirks ); diff --git a/test/vtfpp.cpp b/test/vtfpp.cpp index fcbbbeec1..f2af92d4a 100644 --- a/test/vtfpp.cpp +++ b/test/vtfpp.cpp @@ -1749,3 +1749,39 @@ TEST(vtfpp, distancealpha_wrap_sample) { EXPECT_NE(std::byte{0x00}, sample(mapped, dstW / 2, dstH / 2, dstW, 4, 3)); EXPECT_NE(std::byte{0xFF}, sample(mapped, dstW / 2, dstH / 2, dstW, 4, 3)); } + +TEST(vtfpp, distancealpha_reflect_sample) { + using namespace TestDistanceMapping; + using namespace DistanceMapping; + + std::vector bw(w * h); + for (uint16_t y = h / 2; y < h; y++) { + for (uint16_t x = 0; x < w; x++) { + reference(bw, x, y, w) = 1.f; + } + } + + std::vector mapped = alphaToDistance( + {reinterpret_cast(bw.data()), w * h * sizeof(float)}, + ImageFormat::R32F, + ImageFormat::RGBA8888, + w, + h, + reduceX, + reduceY, + false, + 1.0f, + 0.04f, + Flags::NONE, + Dither::NONE, + ImageConversion::ResizeFilter::NICE, + ImageConversion::ResizeEdge::REFLECT, + nullptr + ); + EXPECT_EQ(std::byte{0x00}, sample(mapped, 0, 0, dstW, 4, 3)); + EXPECT_EQ(std::byte{0x00}, sample(mapped, dstW - 1, 0, dstW, 4, 3)); + EXPECT_EQ(std::byte{0x00}, sample(mapped, dstW / 2, dstH / 4, dstW, 4, 3)); + EXPECT_EQ(std::byte{0xFF}, sample(mapped, dstW / 2, dstH / 4 * 3, dstW, 4, 3)); + EXPECT_NE(std::byte{0x00}, sample(mapped, dstW / 2, dstH / 2, dstW, 4, 3)); + EXPECT_NE(std::byte{0xFF}, sample(mapped, dstW / 2, dstH / 2, dstW, 4, 3)); +}