diff --git a/README.md b/README.md
index d2dd2d681..0f8d09545 100644
--- a/README.md
+++ b/README.md
@@ -524,6 +524,7 @@ These are only the tools and games using `sourcepp` that I know of. If you would
- `vtfpp`
- NICE/Lanczos-3 resize filter's initial implementation was contributed by [@koerismo](https://github.com/koerismo).
- Big-endian support is based on work by [@partyvan](https://github.com/partyvan).
+ - Distance to alpha support was contributed by [@partyvan](https://github.com/partyvan).
- SHT parser/writer was contributed by [@Trico Everfire](https://github.com/Trico-Everfire).
- Initial VTF write support was loosely based on work by [@Trico Everfire](https://github.com/Trico-Everfire).
- HDRI to cubemap conversion code is modified from the [HdriToCubemap](https://github.com/ivarout/HdriToCubemap) library by [@ivarout](https://github.com/ivarout).
diff --git a/docs/index.html b/docs/index.html
index e1de45bcc..4dda27ab3 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -895,6 +895,7 @@
Special Thanks
- NICE/Lanczos-3 resize filter's initial implementation was contributed by @koerismo.
- Big-endian support is based on work by @partyvan.
+ - Distance to alpha support was contributed by @partyvan.
- SHT parser/writer was contributed by @Trico Everfire.
- Initial VTF write support was loosely based on work by @Trico Everfire.
- HDRI to cubemap conversion code is modified from the HdriToCubemap library by @ivarout.
diff --git a/include/vtfpp/DistanceMapping.h b/include/vtfpp/DistanceMapping.h
new file mode 100644
index 000000000..92199a0b1
--- /dev/null
+++ b/include/vtfpp/DistanceMapping.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include
+
+#include
+#include
+
+namespace vtfpp::DistanceMapping {
+
+enum class Dither {
+ NONE = 0, ///< No dithering.
+ GRADIENT_TANGENT, ///< Experimental dithering approach that diffuses quantization error perpendicular to the distance gradient at any destination pixel. Slow and requires up to double the memory.
+ // anything generic that doesn't depend on the geometry of the distance field (floyd-steinberg etc) would belong somewhere more general.
+};
+
+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.
+ 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)
+
+/// In one operation, convert an image's alpha channel, or, for single-channel formats, its only channel, to a VTEX-style distance map, and downscale other channels, if present in the output, according to the given resize parameters.
+/// @return Empty if:
+/// * \p inFormat or \p outFormat do not satisfy their documented predicates,
+/// * \p reduceX or \p reduceY are not powers of two,
+/// * \p distanceSpread would result in a search radius of zero with either \p reduceX or \p reduceY,
+/// * \p alphaThreshold is out of range, or
+/// * \p edge is ResizeEdge::ZERO.
+///
+/// Otherwise, an image at \p width / \p reduceX by \p height / \p reduceY in \p outFormat:
+/// * if \p outFormat is single-channel, R contains the distance map.
+/// * if \p outFormat has alpha, A contains the distance map, while R/G/B contain:
+/// * if \p inFormat had R/G/B, the scaled R/G/B of the input image, otherwise
+/// * 0 in all other channels.
+[[nodiscard]] std::vector alphaToDistance(
+ std::span imageData,
+ ImageFormat inFormat, ///< Any format that is either single-channel, or has an alpha channel.
+ ImageFormat outFormat, ///< The same requirements as inFormat; channel count does not need to correspond to inFormat.
+ uint16_t width,
+ uint16_t height,
+ uint16_t reduceX, ///< Power-of-two horizontal reduction factor.
+ uint16_t reduceY, ///< Power-of-two vertical reduction factor.
+ bool srgb, ///< Used in the case of RGBA->RGBA. premultipliedAlpha is omitted.
+ float distanceSpread = 1.f, ///< Scale factor beyond \p reduceX and \p reduceY, of search radius for distance hits.
+ float alphaThreshold = 0.04f, ///< Threshold below which alpha values are considered zero.
+ Flags flags = Flags::NONE, ///< Additional behaviors that deviate from VTEX but may be useful; see DistanceFlags.
+ Dither dither = Dither::NONE, ///< Internally, distance maps are always computed in floating point. When set to a value other than NONE, and the output format is of integral type, dithering is applied prior to quantization. Depending on the application and contents, this can improve or worsen the quality of the distance map.
+ ImageConversion::ResizeFilter filter = ImageConversion::ResizeFilter::NICE, ///< Default value mimics VTEX. Does not affect the alpha channel; distance mapping is a distinct sampling operation from source to destination space. Entirely unused if input is single-channel.
+ ImageConversion::ResizeEdge edge = ImageConversion::ResizeEdge::CLAMP, ///< Dictates the sampling policy of the distance function regardless of whether there are non-alpha channels to be resized.
+ bool* valveQuirks = nullptr ///< When non-null, mimic VTEX's policy of blanking out any edge pixels in the distance map to 0 (i.e. infinite distance), and report back whether this resulted in any change (such that a command-line tool emulating VTEX would want to report a warning).
+);
+
+} // namespace vtfpp::DistanceMapping
diff --git a/include/vtfpp/vtfpp.h b/include/vtfpp/vtfpp.h
index 6e3c7b09b..2731ba498 100644
--- a/include/vtfpp/vtfpp.h
+++ b/include/vtfpp/vtfpp.h
@@ -5,6 +5,7 @@
* include it the same way as any of the other SourcePP libraries.
*/
+#include "DistanceMapping.h"
#include "HOT.h"
#include "ImageConversion.h"
#include "ImageFormats.h"
diff --git a/lang/c/include/vtfppc/DistanceMapping.h b/lang/c/include/vtfppc/DistanceMapping.h
new file mode 100644
index 000000000..d35266201
--- /dev/null
+++ b/lang/c/include/vtfppc/DistanceMapping.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include
+
+#include "API.h"
+#include "ImageConversion.h"
+
+VTFPP_EXTERN typedef enum {
+ VTFPP_DISTANCE_MAPPING_DITHER_NONE = 0,
+ VTFPP_DISTANCE_MAPPING_DITHER_GRADIENT_TANGENT,
+} vtfpp_distance_mapping_dither_e;
+
+VTFPP_EXTERN typedef enum {
+ VTFPP_DISTANCE_MAPPING_FLAG_NONE = 0,
+ VTFPP_DISTANCE_MAPPING_FLAG_DISTANCEAA = 1 << 0,
+ VTFPP_DISTANCE_MAPPING_FLAG_EUCLIDEAN = 1 << 1,
+ VTFPP_DISTANCE_MAPPING_FLAG_SAMPLECENTERED = 1 << 2,
+} vtfpp_distance_mapping_flags_e;
+
+VTFPP_API sourcepp_buffer_t vtfpp_distance_mapping_alpha_to_distance(const unsigned char* buffer, size_t bufferLen, vtfpp_image_format_e inFormat, vtfpp_image_format_e outFormat, uint16_t width, uint16_t height, uint16_t reduceX, uint16_t reduceY, int srgb); // REQUIRES MANUAL FREE: sourcepp_buffer_free
+VTFPP_API sourcepp_buffer_t vtfpp_distance_mapping_alpha_to_distance_ex(const unsigned char* buffer, size_t bufferLen, vtfpp_image_format_e inFormat, vtfpp_image_format_e outFormat, uint16_t width, uint16_t height, uint16_t reduceX, uint16_t reduceY, int srgb, float distanceSpread, float alphaThreshold, vtfpp_distance_mapping_flags_e flags, vtfpp_distance_mapping_dither_e dither, vtfpp_image_conversion_resize_filter_e filter, vtfpp_image_conversion_resize_edge_e edge, int* valveQuirks); // REQUIRES MANUAL FREE: sourcepp_buffer_free
+
+// C++ conversion routines
+#ifdef __cplusplus
+
+#include
+
+namespace sourceppc::convert {
+
+inline vtfpp::DistanceMapping::Dither cast(vtfpp_distance_mapping_dither_e value) {
+ switch (value) {
+ case VTFPP_DISTANCE_MAPPING_DITHER_NONE: return vtfpp::DistanceMapping::Dither::NONE;
+ case VTFPP_DISTANCE_MAPPING_DITHER_GRADIENT_TANGENT: return vtfpp::DistanceMapping::Dither::GRADIENT_TANGENT;
+ }
+ return vtfpp::DistanceMapping::Dither::NONE;
+}
+
+inline vtfpp_distance_mapping_dither_e cast(vtfpp::DistanceMapping::Dither value) {
+ switch (value) {
+ case vtfpp::DistanceMapping::Dither::NONE: return VTFPP_DISTANCE_MAPPING_DITHER_NONE;
+ case vtfpp::DistanceMapping::Dither::GRADIENT_TANGENT: return VTFPP_DISTANCE_MAPPING_DITHER_GRADIENT_TANGENT;
+ }
+ return VTFPP_DISTANCE_MAPPING_DITHER_NONE;
+}
+
+inline vtfpp::DistanceMapping::Flags cast(vtfpp_distance_mapping_flags_e flags) {
+ return static_cast(flags);
+}
+
+inline vtfpp_distance_mapping_flags_e cast(vtfpp::DistanceMapping::Flags flags) {
+ return static_cast(flags);
+}
+
+} // namespace sourceppc::convert
+
+#endif
diff --git a/lang/c/include/vtfppc/vtfpp.h b/lang/c/include/vtfppc/vtfpp.h
index c00e8ae95..7628f99fb 100644
--- a/lang/c/include/vtfppc/vtfpp.h
+++ b/lang/c/include/vtfppc/vtfpp.h
@@ -5,6 +5,7 @@
* include it the same way as any of the other SourcePP libraries.
*/
+#include "DistanceMapping.h"
#include "HOT.h"
#include "ImageConversion.h"
#include "ImageFormats.h"
diff --git a/lang/c/src/gameppc/_gameppc.cmake b/lang/c/src/gameppc/_gameppc.cmake
index 51a27a444..a660701fa 100644
--- a/lang/c/src/gameppc/_gameppc.cmake
+++ b/lang/c/src/gameppc/_gameppc.cmake
@@ -1,4 +1,5 @@
add_pretty_parser(gamepp C
SOURCES
+ "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/gameppc/API.h"
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/gameppc/gamepp.h"
"${CMAKE_CURRENT_LIST_DIR}/gamepp.cpp")
diff --git a/lang/c/src/steamppc/_steamppc.cmake b/lang/c/src/steamppc/_steamppc.cmake
index 5529bae06..d6e0e2d79 100644
--- a/lang/c/src/steamppc/_steamppc.cmake
+++ b/lang/c/src/steamppc/_steamppc.cmake
@@ -1,4 +1,5 @@
add_pretty_parser(steampp C
SOURCES
+ "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/steamppc/API.h"
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/steamppc/steampp.h"
"${CMAKE_CURRENT_LIST_DIR}/steampp.cpp")
diff --git a/lang/c/src/vcryptppc/_vcryptppc.cmake b/lang/c/src/vcryptppc/_vcryptppc.cmake
index da353e0e8..151408c67 100644
--- a/lang/c/src/vcryptppc/_vcryptppc.cmake
+++ b/lang/c/src/vcryptppc/_vcryptppc.cmake
@@ -1,5 +1,6 @@
add_pretty_parser(vcryptpp C
SOURCES
+ "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vcryptppc/API.h"
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vcryptppc/vcryptpp.h"
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vcryptppc/VFONT.h"
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vcryptppc/VICE.h"
diff --git a/lang/c/src/vpkppc/_vpkppc.cmake b/lang/c/src/vpkppc/_vpkppc.cmake
index 76b2edf42..5de8c538b 100644
--- a/lang/c/src/vpkppc/_vpkppc.cmake
+++ b/lang/c/src/vpkppc/_vpkppc.cmake
@@ -18,6 +18,7 @@ add_pretty_parser(vpkpp C
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/WAD3.h"
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/XZP.h"
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/ZIP.h"
+ "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/API.h"
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/Attribute.h"
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/Entry.h"
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/Options.h"
diff --git a/lang/c/src/vtfppc/DistanceMapping.cpp b/lang/c/src/vtfppc/DistanceMapping.cpp
new file mode 100644
index 000000000..d7759c0c1
--- /dev/null
+++ b/lang/c/src/vtfppc/DistanceMapping.cpp
@@ -0,0 +1,26 @@
+#include
+
+#include
+
+using namespace sourceppc;
+using namespace vtfpp;
+
+VTFPP_API sourcepp_buffer_t vtfpp_distance_mapping_alpha_to_distance(const unsigned char* buffer, size_t bufferLen, vtfpp_image_format_e inFormat, vtfpp_image_format_e outFormat, uint16_t width, uint16_t height, uint16_t reduceX, uint16_t reduceY, int srgb) {
+ SOURCEPP_EARLY_RETURN_VAL(buffer, SOURCEPP_BUFFER_INVALID);
+ SOURCEPP_EARLY_RETURN_VAL(bufferLen, SOURCEPP_BUFFER_INVALID);
+
+ return convert::toBuffer(DistanceMapping::alphaToDistance({reinterpret_cast(buffer), bufferLen}, convert::cast(inFormat), convert::cast(outFormat), width, height, reduceX, reduceY, srgb));
+}
+
+VTFPP_API sourcepp_buffer_t vtfpp_distance_mapping_alpha_to_distance_ex(const unsigned char* buffer, size_t bufferLen, vtfpp_image_format_e inFormat, vtfpp_image_format_e outFormat, uint16_t width, uint16_t height, uint16_t reduceX, uint16_t reduceY, int srgb, float distanceSpread, float alphaThreshold, vtfpp_distance_mapping_flags_e flags, vtfpp_distance_mapping_dither_e dither, vtfpp_image_conversion_resize_filter_e filter, vtfpp_image_conversion_resize_edge_e edge, int* valveQuirks) {
+ SOURCEPP_EARLY_RETURN_VAL(buffer, SOURCEPP_BUFFER_INVALID);
+ SOURCEPP_EARLY_RETURN_VAL(bufferLen, SOURCEPP_BUFFER_INVALID);
+
+ if (!valveQuirks) {
+ return convert::toBuffer(DistanceMapping::alphaToDistance({reinterpret_cast(buffer), bufferLen}, convert::cast(inFormat), convert::cast(outFormat), width, height, reduceX, reduceY, srgb, distanceSpread, alphaThreshold, convert::cast(flags), convert::cast(dither), convert::cast(filter), convert::cast(edge), nullptr));
+ }
+ bool valveQuirksBool = *valveQuirks;
+ const auto out = convert::toBuffer(DistanceMapping::alphaToDistance({reinterpret_cast(buffer), bufferLen}, convert::cast(inFormat), convert::cast(outFormat), width, height, reduceX, reduceY, srgb, distanceSpread, alphaThreshold, convert::cast(flags), convert::cast(dither), convert::cast(filter), convert::cast(edge), &valveQuirksBool));
+ *valveQuirks = valveQuirksBool;
+ return out;
+}
diff --git a/lang/c/src/vtfppc/_vtfppc.cmake b/lang/c/src/vtfppc/_vtfppc.cmake
index 84139d05e..09546f54e 100644
--- a/lang/c/src/vtfppc/_vtfppc.cmake
+++ b/lang/c/src/vtfppc/_vtfppc.cmake
@@ -1,5 +1,7 @@
add_pretty_parser(vtfpp C
PRECOMPILED_HEADERS
+ "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vtfppc/API.h"
+ "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vtfppc/DistanceMapping.h"
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vtfppc/HOT.h"
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vtfppc/ImageConversion.h"
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vtfppc/ImageFormats.h"
@@ -12,6 +14,7 @@ add_pretty_parser(vtfpp C
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vtfppc/VTF.h"
"${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vtfppc/vtfpp.h"
SOURCES
+ "${CMAKE_CURRENT_LIST_DIR}/DistanceMapping.cpp"
"${CMAKE_CURRENT_LIST_DIR}/HOT.cpp"
"${CMAKE_CURRENT_LIST_DIR}/ImageConversion.cpp"
"${CMAKE_CURRENT_LIST_DIR}/ImageFormats.cpp"
diff --git a/lang/csharp/src/vtfpp/DLL.cs b/lang/csharp/src/vtfpp/DLL.cs
index c662d6e87..1225a6d18 100644
--- a/lang/csharp/src/vtfpp/DLL.cs
+++ b/lang/csharp/src/vtfpp/DLL.cs
@@ -7,6 +7,12 @@ internal static partial class DLL
{
private const string Name = "sourcepp_vtfppc";
+ [LibraryImport(Name)]
+ public static partial sourcepp.DLL.Buffer vtfpp_distance_mapping_alpha_to_distance(ReadOnlySpan buffer, ulong bufferLen, ImageFormat inFormat, ImageFormat outFormat, ushort width, ushort height, ushort reduceX, ushort reduceY, int srgb);
+
+ [LibraryImport(Name)]
+ public static partial sourcepp.DLL.Buffer vtfpp_distance_mapping_alpha_to_distance_ex(ReadOnlySpan buffer, ulong bufferLen, ImageFormat inFormat, ImageFormat outFormat, ushort width, ushort height, ushort reduceX, ushort reduceY, int srgb, float distanceSpread, float alphaThreshold, DistanceMapping.Flags flags, DistanceMapping.Dither dither, ImageConversion.ResizeFilter filter, ImageConversion.ResizeEdge edge, nint valveQuirks);
+
[LibraryImport(Name)]
public static partial nint vtfpp_hot_create();
diff --git a/lang/csharp/src/vtfpp/DistanceMapping.cs b/lang/csharp/src/vtfpp/DistanceMapping.cs
new file mode 100644
index 000000000..2e6f039a1
--- /dev/null
+++ b/lang/csharp/src/vtfpp/DistanceMapping.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace sourcepp.vtfpp;
+
+public static class DistanceMapping
+{
+ public enum Dither
+ {
+ NONE = 0,
+ GRADIENT_TANGENT,
+ }
+
+ public enum Flags : uint
+ {
+ NONE = 0,
+ DISTANCEAA = 1 << 0,
+ EUCLIDEAN = 1 << 1,
+ SAMPLECENTERED = 1 << 2,
+ }
+
+ public static byte[] AlphaToDistance(ReadOnlySpan buffer, ImageFormat inFormat, ImageFormat outFormat, ushort width, ushort height, ushort resizeX, ushort resizeY, bool srgb, ref bool valveQuirks, float distanceSpread = 1.0f, float alphaThreshold = 0.04f, Flags flags = Flags.NONE, Dither dither = Dither.NONE, ImageConversion.ResizeFilter filter = ImageConversion.ResizeFilter.NICE, ImageConversion.ResizeEdge edge = ImageConversion.ResizeEdge.CLAMP)
+ {
+ var valveQuirksInt = Convert.ToInt32(valveQuirks);
+ unsafe
+ {
+ var valveQuirksIntPtr = &valveQuirksInt;
+ var output = new sourcepp.Buffer(DLL.vtfpp_distance_mapping_alpha_to_distance_ex(buffer, (ulong)buffer.Length, inFormat, outFormat, width, height, resizeX, resizeY, Convert.ToInt32(srgb), distanceSpread, alphaThreshold, flags, dither, filter, edge, valveQuirks ? (nint)valveQuirksIntPtr : 0)).Read();
+ valveQuirks = Convert.ToBoolean(valveQuirksInt);
+ return output;
+ }
+ }
+}
diff --git a/lang/csharp/test/vpkpp/PackFileTest.cs b/lang/csharp/test/vpkpp/PackFileTest.cs
index 380ad3fa1..143daa4cd 100644
--- a/lang/csharp/test/vpkpp/PackFileTest.cs
+++ b/lang/csharp/test/vpkpp/PackFileTest.cs
@@ -25,7 +25,7 @@ public void Open()
Assert.IsFalse(vpk.IsReadOnly);
- Assert.AreEqual(3509u, vpk.EntryCount());
+ Assert.AreEqual(3524u, vpk.EntryCount());
Assert.AreEqual(BasePortalPath + "portal_pak_dir.vpk", vpk.Filepath);
Assert.AreEqual(BasePortalPath + "portal_pak", vpk.TruncatedFilepath);
diff --git a/lang/python/src/vtfpp.h b/lang/python/src/vtfpp.h
index 2e7cd3ec1..eb3744758 100644
--- a/lang/python/src/vtfpp.h
+++ b/lang/python/src/vtfpp.h
@@ -321,6 +321,26 @@ inline void register_python(py::module_& m) {
}, "image_data"_a, "format"_a, "width"_a, "height"_a);
}
+ {
+ using namespace DistanceMapping;
+ auto DistanceMapping = vtfpp.def_submodule("DistanceMapping");
+
+ py::enum_(DistanceMapping, "Dither")
+ .value("NONE", Dither::NONE)
+ .value("GRADIENT_TANGENT", Dither::GRADIENT_TANGENT);
+
+ py::enum_(DistanceMapping, "Flags", py::is_flag())
+ .value("NONE", Flags::NONE)
+ .value("DISTANCEAA", Flags::DISTANCEAA)
+ .value("EUCLIDEAN", Flags::EUCLIDEAN)
+ .value("SAMPLECENTERED", Flags::SAMPLECENTERED);
+
+ DistanceMapping.def("alpha_to_distance", [](const py::bytes& imageData, ImageFormat inFormat, ImageFormat outFormat, uint16_t width, uint16_t height, uint16_t reduceX, uint16_t reduceY, bool srgb, float distanceSpread = 1.f, float alphaThreshold = 0.04f, Flags flags = Flags::NONE, Dither dither = Dither::NONE, ImageConversion::ResizeFilter filter = ImageConversion::ResizeFilter::NICE, ImageConversion::ResizeEdge edge = ImageConversion::ResizeEdge::CLAMP, bool enableValveQuirks = false) {
+ const auto d = alphaToDistance({static_cast(imageData.data()), static_cast(imageData.data()) + imageData.size()}, inFormat, outFormat, width, height, reduceX, reduceY, srgb, distanceSpread, alphaThreshold, flags, dither, filter, edge, enableValveQuirks ? &enableValveQuirks : nullptr);
+ return std::pair{py::bytes{d.data(), d.size()}, enableValveQuirks};
+ }, "image_data"_a, "in_format"_a, "out_format"_a, "width"_a, "height"_a, "reduce_x"_a, "reduce_y"_a, "srgb"_a, "distance_spread"_a = 1.f, "alpha_threshold"_a = 0.04f, "flags"_a = Flags::NONE, "dither"_a = Dither::NONE, "filter"_a = ImageConversion::ResizeFilter::NICE, "edge"_a = ImageConversion::ResizeEdge::CLAMP, "enable_valve_quirks"_a = false);
+ }
+
{
using namespace ImageQuantize;
auto ImageQuantize = vtfpp.def_submodule("ImageQuantize");
diff --git a/src/vtfpp/DistanceMapping.cpp b/src/vtfpp/DistanceMapping.cpp
new file mode 100644
index 000000000..62e74b7e3
--- /dev/null
+++ b/src/vtfpp/DistanceMapping.cpp
@@ -0,0 +1,528 @@
+#include
+
+#include
+
+#include
+#include
+#include
+
+using namespace sourcepp;
+using namespace vtfpp;
+
+namespace vtfpp::DistanceMapping::detail {
+namespace {
+
+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)));
+}
+
+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;
+}
+
+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 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;
+}
+
+IndexFunction indexBy(ImageConversion::ResizeEdge edge)
+{
+ switch (edge) {
+ default:
+ break;
+ case ImageConversion::ResizeEdge::REFLECT:
+ return indexReflected;
+ case ImageConversion::ResizeEdge::WRAP:
+ return indexWrapped;
+ }
+ return indexClamped;
+}
+
+// big ugly immutable record of everything pertaining to how we read alpha from an image
+class SampleParam {
+public:
+ SampleParam(
+ const float *srcImg,
+ uint16_t inWidth,
+ uint16_t inHeight,
+ uint16_t reduceX,
+ uint16_t reduceY,
+ uint8_t pxLen,
+ uint8_t alphaOffs,
+ float distanceSpread,
+ bool sampleCentered,
+ float alphaThreshold,
+ ImageConversion::ResizeEdge edge
+ )
+ : srcImg(srcImg)
+ , pxLen(pxLen)
+ , alphaOffs(alphaOffs)
+ , imgWidth(inWidth)
+ , imgHeight(inHeight)
+ , 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))
+ {}
+
+ float sample(Vec2i v) const {
+ return this->srcImg[this->indexFn(v, this->imgWidth, this->imgHeight, this->pxLen, this->alphaOffs)];
+ }
+
+ Leaf thresh(float alpha) const {
+ return alpha >= this->alphaThreshold ? IN : OUT;
+ }
+
+ const float *const srcImg;
+ const float alphaThreshold;
+ const uint16_t imgHeight;
+ const uint16_t imgWidth;
+ 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;
+};
+
+// 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);
+ }
+ }
+ };
+
+ static bool brancheq(Branch a, Branch b) {
+ return holds_alternative(a) && holds_alternative(b) && get(a) == get(b);
+ }
+
+ 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;
+ }
+ }
+ }
+
+ return curShade;
+ }
+
+ 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);
+
+ if (brancheq(iNW, iNE) && brancheq(iNE, iSW) && brancheq(iSW, iSE)) {
+ return get(iNW);
+ }
+ return new Node(iNW, iNE, iSW, iSE);
+ }
+};
+
+// 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();
+ }
+ 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();
+ }
+
+ if (this->curX >= this->imgWidth) {
+ this->curX = this->offsX;
+ this->curY += this->reduceY;
+ }
+
+ return this->curY < this->imgHeight;
+ }
+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);
+ }
+
+ 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,
+ float *dstImg,
+ 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;
+
+ 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;
+ }
+ 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) {
+ continue;
+ }
+ nearest = fmin(nearest, dist);
+ }
+ }
+ }
+
+ nearest = fmin(0.5f, nearest * 0.5f / fRadius);
+ if (stateRef == OUT) {
+ nearest = -nearest;
+ }
+
+ dstImg[iOut] = 0.5 + nearest;
+ if (tangentDiffusion) {
+ gradients[iGradW] = lastAng;
+ }
+ }
+ }
+ } while (keepPainting);
+
+ if (tangentDiffusion) {
+ // attempt to diffuse quantization error tangent to the gradient of the
+ // distance map. you could imagine some approach that involves grouping
+ // greyscale pixels into distinct contours, and playing ring around the
+ // rosy for each group, but there's no practical way to avoid littering
+ // residual bits of error into pixels you've already quantized, ruining
+ // the whole point. the usual suspects like floyd-steinberg use kernels
+ // shaped to only write *ahead* of a conventional raster scan. here, we
+ // do the same by determining a shape, bounded by the following "mask":
+ //
+ // ⎡ 0 0 0 ⎤
+ // ⎢ 0 0 1 ⎥
+ // ⎣ 1 1 1 ⎦,
+ //
+ // mapping adjacent pixels to the unit circle's bounding box like this:
+ //
+ // ⎡ NW:(-1, 1) N:(0, 1) NE:(1, 1) ⎤
+ // ⎢ W:(-1, 0) E:(1, 0) ⎥
+ // ⎣ SW:(-1, -1) S:(0, -1) SE:(1, -1) ⎦,
+ //
+ // choose the two points nearest (x, y) on the unit circle at θ + π / 2
+ // (i.e. the right-hand normal of the gradient direction θ). distribute
+ // the quantization error E between the two, weighted by the opposite's
+ // distance over the sum of both distances (it feels as though a better
+ // weighting should exist but that's what i get for dropping out), then
+ // fold the whole thing over to only entries ahead of us in scan order:
+ //
+ // ⎡ ⎤
+ // k = ⎢ E+W ⎥
+ // ⎣ 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)
+ static constexpr std::array hardSigns{1, 1, 0, -1, -1, -1, 0, 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
+ // we wanted SW..E, we'd index into {0, 5, 6, 7}.
+ 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) {
+ continue;
+ }
+ float thetaNormal = fmod(theta + 2.5f * pi_f32, 2.0f * pi_f32);
+ int8_t nEighths = (thetaNormal - fmod(thetaNormal, eighth)) / eighth;
+ float quantErr = fmod(dstImg[diffOut], *tangentDiffusion);
+ dstImg[diffOut] -= quantErr;
+ bool atRight = rightEdge && i >= diffW - 1;
+ bool bottomEdge = diffY >= dstHeight - 1;
+ if (atRight && bottomEdge) {
+ break;
+ }
+ Vec2f32 normal{cos(thetaNormal), sin(thetaNormal)};
+ // distance calculation uses the whole circle
+ float dist0 = vecDist(normal, {hardSigns[(nEighths + 0) % 8], hardSigns[(nEighths + 2) % 8]});
+ float dist1 = vecDist(normal, {hardSigns[(nEighths + 1) % 8], hardSigns[(nEighths + 3) % 8]});
+ // offset selection uses the part of the circle that is yet to be scanned
+ Vec2i offs0{hardSigns[(nEighths + 0) % 4], hardSigns[(nEighths + 2) % 4]};
+ Vec2i offs1{hardSigns[(nEighths + 1) % 4], hardSigns[(nEighths + 3) % 4]};
+ 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);
+ }
+ } while (keepDiffusing);
+ }
+
+ if (valveQuirks) {
+ *valveQuirks = false;
+
+ auto blackOutAndWarn = [&](auto oi) {
+ float& px = dstImg[oi * dstPxLen + dstAlphaOffs];
+ if (fabs(px) > 0.000001f) {
+ *valveQuirks = true;
+ }
+ px = 0.0f;
+ };
+
+ for (auto x = 0; x < dstWidth; x++) {
+ blackOutAndWarn(x);
+ blackOutAndWarn(x + (dstHeight - 1) * dstWidth);
+ }
+ for (auto y = 0; y < dstHeight; y++) {
+ blackOutAndWarn(y * dstWidth);
+ blackOutAndWarn(y * dstWidth + dstHeight - 1);
+ }
+ }
+}
+
+bool operator!(Flags flags) {
+ return flags == Flags::NONE;
+};
+
+bool isSingleChannel(ImageFormat format) {
+ using namespace ImageFormatDetails;
+ return red(format) == bpp(format) && bpp(format);
+}
+
+} // namespace
+} // namespace vtfpp::DistanceMapping::detail
+
+[[nodiscard]] std::vector DistanceMapping::alphaToDistance(std::span imageData, ImageFormat inFormat, ImageFormat outFormat, uint16_t width, uint16_t height, uint16_t reduceX, uint16_t reduceY, bool srgb, float distanceSpread, float alphaThreshold, DistanceMapping::Flags flags, DistanceMapping::Dither dither, ImageConversion::ResizeFilter filter, ImageConversion::ResizeEdge edge, bool *valveQuirks) {
+ using namespace vtfpp::DistanceMapping::detail;
+ using namespace ImageFormatDetails;
+
+ auto mkFormat = [](ImageFormat format) {
+ return isSingleChannel(format) ?
+ std::make_tuple(ImageFormat::R32F, uint8_t{1}, uint8_t{0})
+ : (decompressedAlpha(format) || alpha(format)) ?
+ std::make_tuple(ImageFormat::RGBA32323232F, uint8_t{4}, uint8_t{3})
+ :
+ std::make_tuple(ImageFormat::EMPTY, uint8_t{0}, uint8_t{0});
+ };
+ auto [intermediateRead, srcPxLen, srcAlphaOffs] = mkFormat(inFormat);
+ auto [intermediateWrite, dstPxLen, dstAlphaOffs] = mkFormat(outFormat);
+
+ if (
+ intermediateRead == ImageFormat::EMPTY
+ || intermediateWrite == ImageFormat::EMPTY
+ || !math::isPowerOf2(reduceX)
+ || !math::isPowerOf2(reduceY)
+ || ftoabsu16(distanceSpread * reduceX) < 1
+ || ftoabsu16(distanceSpread * reduceY) < 1
+ || std::clamp(alphaThreshold, 0.0f, 1.0f) != alphaThreshold
+ || edge == ImageConversion::ResizeEdge::ZERO
+ ) {
+ return {};
+ }
+
+ bool inputNeedsConversion = intermediateRead != inFormat;
+ std::vector i_love_raii = inputNeedsConversion ? convertImageDataToFormat(imageData, inFormat, intermediateRead, width, height) : std::vector(0);
+ std::span srcImg = inputNeedsConversion ? i_love_raii : imageData;
+
+ SampleParam param(reinterpret_cast(srcImg.data()), width, height, reduceX, reduceY, srcPxLen, srcAlphaOffs, distanceSpread, !!(flags & Flags::SAMPLECENTERED), alphaThreshold, edge);
+
+ uint16_t dstWidth = width / reduceX, dstHeight = height / reduceY;
+
+ std::vector dstImg
+ = (!isSingleChannel(inFormat) && !isSingleChannel(outFormat)) ?
+ convertImageDataToFormat(
+ resizeImageData(imageData, inFormat, width, dstWidth, height, dstHeight, srgb, true, filter, edge),
+ inFormat, intermediateWrite, dstWidth, dstHeight
+ )
+ :
+ std::vector(sizeof(float) * dstPxLen * dstWidth * dstHeight, std::byte{0x00});
+
+ float quantum = 1.0f / static_cast(1 << decompressedAlpha(outFormat));
+ bool doGradientDither = dither == Dither::GRADIENT_TANGENT && !decimal(outFormat);
+ paintMap(
+ ¶m,
+ reinterpret_cast(dstImg.data()),
+ dstPxLen,
+ dstAlphaOffs,
+ !!(flags & Flags::DISTANCEAA),
+ !!(flags & Flags::EUCLIDEAN),
+ doGradientDither ? &quantum : nullptr,
+ valveQuirks
+ );
+
+ return (outFormat == intermediateWrite) ? dstImg : convertImageDataToFormat(dstImg, intermediateWrite, outFormat, dstWidth, dstHeight);
+}
diff --git a/src/vtfpp/_vtfpp.cmake b/src/vtfpp/_vtfpp.cmake
index 9be5bb3a3..9735c2039 100644
--- a/src/vtfpp/_vtfpp.cmake
+++ b/src/vtfpp/_vtfpp.cmake
@@ -1,6 +1,7 @@
add_pretty_parser(vtfpp
DEPS bcdec libzstd_static miniz sourcepp_compression sourcepp_parser sourcepp_stb
PRECOMPILED_HEADERS
+ "${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/DistanceMapping.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/HOT.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/ImageConversion.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/ImageFormats.h"
@@ -14,6 +15,7 @@ add_pretty_parser(vtfpp
"${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/VTF.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/vtfpp.h"
SOURCES
+ "${CMAKE_CURRENT_LIST_DIR}/DistanceMapping.cpp"
"${CMAKE_CURRENT_LIST_DIR}/HOT.cpp"
"${CMAKE_CURRENT_LIST_DIR}/ImageConversion.cpp"
"${CMAKE_CURRENT_LIST_DIR}/ImageQuantize.cpp"
diff --git a/test/vtfpp.cpp b/test/vtfpp.cpp
index 50505be1b..64da9f141 100644
--- a/test/vtfpp.cpp
+++ b/test/vtfpp.cpp
@@ -1574,3 +1574,135 @@ TEST(vtfpp, read_v76_nomip_c9) {
EXPECT_EQ(image->flags, Resource::FLAG_NONE);
EXPECT_EQ(image->data.size(), ImageFormatDetails::getDataLength(vtf.getFormat(), vtf.getMipCount(), vtf.getFrameCount(), vtf.getFaceCount(), vtf.getWidth(), vtf.getHeight(), vtf.getDepth()));
}
+
+namespace TestDistanceMapping {
+
+constexpr uint16_t w = 1024;
+constexpr uint16_t h = 1024;
+constexpr uint16_t reduceX = 4;
+constexpr uint16_t reduceY = 4;
+constexpr uint16_t dstW = w / reduceX;
+constexpr uint16_t dstH = h / reduceY;
+
+template
+[[nodiscard]] T& reference(std::span s, uint16_t x_, uint16_t y_, uint16_t width, uint8_t pxLen = 1, uint8_t alphaOffs = 0) {
+ return s[pxLen * (y_ * width + x_) + alphaOffs];
+}
+
+[[nodiscard]] std::byte sample(std::span s, uint16_t x_, uint16_t y_, uint16_t width, uint8_t pxLen = 1, uint8_t alphaOffs = 0) {
+ return s[pxLen * (y_ * width + x_) + alphaOffs];
+}
+
+} // namespace TestDistanceMapping
+
+TEST(vtfpp, distancealpha_edge_mask_true) {
+ 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, h) = 1.f;
+ }
+ }
+
+ bool valveQuirks = false;
+
+ 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::CLAMP,
+ &valveQuirks
+ );
+ EXPECT_EQ(std::byte{0x00}, sample(mapped, 0, 0, dstW, 4, 3));
+ EXPECT_EQ(std::byte{0x00}, sample(mapped, dstW - 1, dstH - 1, 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));
+ ASSERT_TRUE(valveQuirks);
+}
+
+TEST(vtfpp, distancealpha_edge_mask_false) {
+ using namespace TestDistanceMapping;
+ using namespace DistanceMapping;
+
+ std::vector circle(w * h);
+ for (uint16_t y = 0; y < h; y++) {
+ for (uint16_t x = 0; x < w; x++) {
+ if (std::hypot(fabs(512 - y), fabs(512 - x)) < 320.f) {
+ reference(circle, x, y, h) = 1.f;
+ }
+ }
+ }
+
+ bool valveQuirks = true;
+
+ std::vector mapped = alphaToDistance(
+ {reinterpret_cast(circle.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::CLAMP,
+ &valveQuirks
+ );
+ EXPECT_EQ(std::byte{0x00}, sample(mapped, 0, 0, dstW, 4, 3));
+ EXPECT_EQ(std::byte{0x00}, sample(mapped, dstW - 1, dstH - 1, dstW, 4, 3));
+ EXPECT_EQ(std::byte{0xFF}, sample(mapped, dstW / 2, dstH / 2, dstW, 4, 3));
+ ASSERT_FALSE(valveQuirks);
+}
+
+TEST(vtfpp, distancealpha_wrap_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, h) = 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::WRAP,
+ nullptr
+ );
+ EXPECT_NE(std::byte{0x00}, sample(mapped, 0, 0, dstW, 4, 3));
+ EXPECT_NE(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));
+}