From e9907a80ecaa2c6c55208fb8ea3a482103bfb908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Thu, 9 Oct 2025 18:27:38 +0200 Subject: [PATCH] TextureTools: new utilities for sampling textures on a CPU. I need this to sample a color map for debug visualization in the UI library. Thus so far it's just 1D, and with 8-bit input. Other variants might get added in the future if needed. --- doc/changelog.dox | 3 + src/Magnum/DebugTools/ColorMap.h | 12 +- src/Magnum/TextureTools/CMakeLists.txt | 4 +- src/Magnum/TextureTools/Sample.cpp | 97 +++++++++ src/Magnum/TextureTools/Sample.h | 87 ++++++++ src/Magnum/TextureTools/Test/CMakeLists.txt | 2 + src/Magnum/TextureTools/Test/SampleTest.cpp | 227 ++++++++++++++++++++ 7 files changed, 426 insertions(+), 6 deletions(-) create mode 100644 src/Magnum/TextureTools/Sample.cpp create mode 100644 src/Magnum/TextureTools/Sample.h create mode 100644 src/Magnum/TextureTools/Test/SampleTest.cpp diff --git a/doc/changelog.dox b/doc/changelog.dox index 5fd350e74..e94366f78 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -507,6 +507,9 @@ See also: - New @ref TextureTools::atlasTextureCoordinateTransformation() helper for creating an appropriate texture coordinate transformation matrix for textures placed into an atlas +- New @ref TextureTools::sampleLinear(), @ref TextureTools::sampleSrgb() and + @ref TextureTools::sampleSrgbAlpha() utilities for sampling 1D textures + such as colormaps on a CPU - Added a @ref TextureTools::DistanceFieldGL::operator()() overload taking a @ref GL::Framebuffer instead of a @ref GL::Texture as an output for an easier ability to download the resulting image on OpenGL ES platforms; diff --git a/src/Magnum/DebugTools/ColorMap.h b/src/Magnum/DebugTools/ColorMap.h index 064521c52..d6ac0ed4b 100644 --- a/src/Magnum/DebugTools/ColorMap.h +++ b/src/Magnum/DebugTools/ColorMap.h @@ -140,14 +140,16 @@ See @ref building, @ref cmake and @ref debug-tools for more information. @endparblock -For all color maps the returned data is the sRGB colorspace. Desired usage is -by uploading to a texture with linear filtering, depending on the use case with -either clamp or repeat wrapping. For a sRGB workflow don't forget to set the -texture format to sRGB, to ensure the values are interpreted and interpolated -done correctly. +For all color maps the returned data is the sRGB colorspace. Desired GPU usage +is by uploading to a texture with linear filtering, depending on the use case +with either clamp or repeat wrapping. For a sRGB workflow don't forget to set +the texture format to sRGB, to ensure the values are interpreted and +interpolated done correctly. @snippet DebugTools-gl.cpp ColorMap +For CPU-side usage see @ref TextureTools::sampleLinear() and +@ref TextureTools::sampleSrgb(). */ namespace ColorMap { diff --git a/src/Magnum/TextureTools/CMakeLists.txt b/src/Magnum/TextureTools/CMakeLists.txt index 283c3ca1f..7a4c5d7d4 100644 --- a/src/Magnum/TextureTools/CMakeLists.txt +++ b/src/Magnum/TextureTools/CMakeLists.txt @@ -41,10 +41,12 @@ set(CMAKE_FOLDER "Magnum/TextureTools") find_package(Corrade REQUIRED PluginManager) set(MagnumTextureTools_GracefulAssert_SRCS - Atlas.cpp) + Atlas.cpp + Sample.cpp) set(MagnumTextureTools_HEADERS Atlas.h + Sample.h TextureTools.h visibility.h) diff --git a/src/Magnum/TextureTools/Sample.cpp b/src/Magnum/TextureTools/Sample.cpp new file mode 100644 index 000000000..b6e6a08a7 --- /dev/null +++ b/src/Magnum/TextureTools/Sample.cpp @@ -0,0 +1,97 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023, 2024, 2025 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "Sample.h" + +#include +#include + +#include "Magnum/Math/Color.h" + +namespace Magnum { namespace TextureTools { + +namespace { + +template Out sampleInternal( + #ifndef CORRADE_NO_ASSERT + const char* messagePrefix, + #endif + const Containers::StridedArrayView1D& texture, Float factor) +{ + CORRADE_ASSERT(!texture.isEmpty(), + messagePrefix << "expected texture to have at least one element", {}); + CORRADE_ASSERT(factor >= 0.0f && factor <= 1.0f, + messagePrefix << "expected factor to be within the [0, 1] range but got" << factor, {}); + + /* If we're exactly at the end or the texture has just a single element, + return the last element */ + if(factor == 1.0f || texture.size() == 1) + return unpacker(texture.back()); + + /* Otherwise it's an interpolation of two values */ + const Float sample = factor*(texture.size() - 1); + const UnsignedInt index = sample; + return Math::lerp( + unpacker(texture[index]), + unpacker(texture[index + 1]), + sample - index); +} + +} + +Color3 sampleLinear(const Containers::StridedArrayView1D& texture, Float factor) { + return sampleInternal, Math::unpack>( + #ifndef CORRADE_NO_ASSERT + "TextureTools::sampleLinear():", + #endif + texture, factor); +} + +Color4 sampleLinear(const Containers::StridedArrayView1D& texture, Float factor) { + return sampleInternal, Math::unpack>( + #ifndef CORRADE_NO_ASSERT + "TextureTools::sampleLinear():", + #endif + texture, factor); +} + +Color3 sampleSrgb(const Containers::StridedArrayView1D& texture, Float factor) { + return sampleInternal, Color3::fromSrgb>( + #ifndef CORRADE_NO_ASSERT + "TextureTools::sampleSrgb():", + #endif + texture, factor); +} + +Color4 sampleSrgbAlpha(const Containers::StridedArrayView1D& texture, Float factor) { + return sampleInternal, Color4::fromSrgbAlpha>( + #ifndef CORRADE_NO_ASSERT + "TextureTools::sampleSrgbAlpha():", + #endif + texture, factor); +} + +}} diff --git a/src/Magnum/TextureTools/Sample.h b/src/Magnum/TextureTools/Sample.h new file mode 100644 index 000000000..8930f5e65 --- /dev/null +++ b/src/Magnum/TextureTools/Sample.h @@ -0,0 +1,87 @@ +#ifndef Magnum_TextureTools_Sample_h +#define Magnum_TextureTools_Sample_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023, 2024, 2025 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/** @file + * @brief Function @ref Magnum::TextureTools::sampleLinear(), @ref Magnum::TextureTools::sampleSrgb(), @ref Magnum::TextureTools::sampleSrgbAlpha() + * @m_since_latest + */ + +#include "Magnum/Magnum.h" +#include "Magnum/TextureTools/visibility.h" + +namespace Magnum { namespace TextureTools { + +/** +@brief Sample a 1D RGB texture with linear interpolation +@m_since_latest + +Expects that @p texture has at least one element and @p factor is in a +@f$ [0, 1] @f$ range. For a factor of @cpp 0.0f @ce returns the first element +of @p texture, for @cpp 1.0f @ce returns the last, values in between are a +linear interpolation of two nearest elements that are first unpacked to a +floating-point type. + +Note that the @p texture is interpreted as having linear colors. Use +@ref sampleSrgb() if you want to perform conversion from sRGB values instead. +@see @ref Math::lerp(const T&, const T&, U), @ref Math::unpack(const Integral&) +*/ +MAGNUM_TEXTURETOOLS_EXPORT Color3 sampleLinear(const Containers::StridedArrayView1D& texture, Float factor); + +/** +@brief Sample a 1D RGBA texture with linear interpolation +@m_since_latest + +Like @ref sampleLinear(const Containers::StridedArrayView1D&, Float) +but with a four-component input. +*/ +MAGNUM_TEXTURETOOLS_EXPORT Color4 sampleLinear(const Containers::StridedArrayView1D& texture, Float factor); + +/** +@brief Sample a 1D RGB texture with sRGB interpolation +@m_since_latest + +Compared to @ref sampleLinear(const Containers::StridedArrayView1D&, Float) +treats the input values as sRGB and applies @ref Color3::fromSrgb() instead of +@ref Math::unpack(const Integral&). +*/ +MAGNUM_TEXTURETOOLS_EXPORT Color3 sampleSrgb(const Containers::StridedArrayView1D& texture, Float factor); + +/** +@brief Sample a 1D RGBA texture with sRGB interpolation +@m_since_latest + +Compared to @ref sampleLinear(const Containers::StridedArrayView1D&, Float) +treats the input RGB channels as sRGB and applies @ref Color4::fromSrgbAlpha() +instead of @ref Math::unpack(const Integral&). The alpha channel is treated by +that function as linear. +*/ +MAGNUM_TEXTURETOOLS_EXPORT Color4 sampleSrgbAlpha(const Containers::StridedArrayView1D& texture, Float factor); + +}} + +#endif diff --git a/src/Magnum/TextureTools/Test/CMakeLists.txt b/src/Magnum/TextureTools/Test/CMakeLists.txt index 6a926d3c7..32034438b 100644 --- a/src/Magnum/TextureTools/Test/CMakeLists.txt +++ b/src/Magnum/TextureTools/Test/CMakeLists.txt @@ -88,6 +88,8 @@ else() endif() endif() +corrade_add_test(TextureToolsSampleTest SampleTest.cpp LIBRARIES MagnumTextureToolsTestLib) + if(MAGNUM_TARGET_GL) corrade_add_test(TextureToolsDistanceFieldGL_Test DistanceFieldGL_Test.cpp LIBRARIES MagnumTextureTools) diff --git a/src/Magnum/TextureTools/Test/SampleTest.cpp b/src/Magnum/TextureTools/Test/SampleTest.cpp new file mode 100644 index 000000000..cb4a48f45 --- /dev/null +++ b/src/Magnum/TextureTools/Test/SampleTest.cpp @@ -0,0 +1,227 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023, 2024, 2025 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include +#include + +#include "Magnum/Math/Color.h" +#include "Magnum/TextureTools/Sample.h" + +namespace Magnum { namespace TextureTools { namespace Test { namespace { + +struct SampleTest: TestSuite::Tester { + explicit SampleTest(); + + void sample1DSingleElementLinearRgb(); + void sample1DSingleElementLinearRgba(); + void sample1DSingleElementSrgb(); + void sample1DSingleElementSrgbAlpha(); + + void sample1DLinearRgb(); + void sample1DLinearRgba(); + void sample1DSrgb(); + void sample1DSrgbAlpha(); + + void sample1DInvalid(); +}; + +using namespace Math::Literals; + +SampleTest::SampleTest() { + addTests({&SampleTest::sample1DSingleElementLinearRgb, + &SampleTest::sample1DSingleElementLinearRgba, + &SampleTest::sample1DSingleElementSrgb, + &SampleTest::sample1DSingleElementSrgbAlpha, + + &SampleTest::sample1DLinearRgb, + &SampleTest::sample1DLinearRgba, + &SampleTest::sample1DSrgb, + &SampleTest::sample1DSrgbAlpha, + + &SampleTest::sample1DInvalid}); +} + +void SampleTest::sample1DSingleElementLinearRgb() { + /* Should work also with just the Vector base, not just Color3 */ + Vector3ub texture[]{ + 0xff3366_rgb + }; + + /* All factors return the same value */ + CORRADE_COMPARE(sampleLinear(texture, 0.0f), 0xff3366_rgbf); + CORRADE_COMPARE(sampleLinear(texture, 0.6375f), 0xff3366_rgbf); + CORRADE_COMPARE(sampleLinear(texture, 1.0f), 0xff3366_rgbf); +} + +void SampleTest::sample1DSingleElementLinearRgba() { + /* Should work also with just the Vector base, not just Color3 */ + Vector4ub texture[]{ + 0xff336699_rgba + }; + + /* All factors return the same value */ + CORRADE_COMPARE(sampleLinear(texture, 0.0f), 0xff336699_rgbaf); + CORRADE_COMPARE(sampleLinear(texture, 0.6375f), 0xff336699_rgbaf); + CORRADE_COMPARE(sampleLinear(texture, 1.0f), 0xff336699_rgbaf); +} + +void SampleTest::sample1DSingleElementSrgb() { + /* Should work also with just the Vector base, not just Color3 */ + Vector3ub texture[]{ + 0xff3366_srgb + }; + + /* All factors return the same value, converted from sRGB */ + CORRADE_COMPARE(sampleSrgb(texture, 0.0f), 0xff3366_srgbf); + CORRADE_COMPARE(sampleSrgb(texture, 0.6375f), 0xff3366_srgbf); + CORRADE_COMPARE(sampleSrgb(texture, 1.0f), 0xff3366_srgbf); +} + +void SampleTest::sample1DSingleElementSrgbAlpha() { + /* Should work also with just the Vector base, not just Color3 */ + Vector4ub texture[]{ + 0xff336699_srgba + }; + + /* All factors return the same value, converted from sRGB */ + CORRADE_COMPARE(sampleSrgbAlpha(texture, 0.0f), 0xff336699_srgbaf); + CORRADE_COMPARE(sampleSrgbAlpha(texture, 0.6375f), 0xff336699_srgbaf); + CORRADE_COMPARE(sampleSrgbAlpha(texture, 1.0f), 0xff336699_srgbaf); +} + +/* Not const because slice() wouldn't work in that case due to the const + variant being constexpr and thus not returning a reinterpreted reference + (lol ffs) */ +Color4ub Texture[]{ + 0xff336699_rgba, /* 0.0 */ + 0xdeadbeef_rgba, /* 0.25 */ + 0x2200eeff_rgba, /* 0.5 */ + 0xaaccaa33_rgba, /* 0.75 */ + 0x996633ff_rgba, /* 1.0 */ +}; + +void SampleTest::sample1DLinearRgb() { + Containers::StridedArrayView1D texture = Containers::stridedArrayView(Texture).slice(&Color4ub::rgb); + + /* These should return exact values */ + CORRADE_COMPARE(sampleLinear(texture, 0.0f), 0xff3366_rgbf); + CORRADE_COMPARE(sampleLinear(texture, 0.25f), 0xdeadbe_rgbf); + /* This one should not attempt to lerp with the sentinel value */ + /** @todo once a variant with float input exists, put NaNs in a sentinel */ + CORRADE_COMPARE(sampleLinear(texture, 1.0f), 0x996633_rgbf); + + /* This is an exact 25% / 75% interpolation between element 2 and 3 */ + CORRADE_COMPARE(sampleLinear(texture, 0.5f + 0.0625f), 0x4433dd_rgbf); + CORRADE_COMPARE(sampleLinear(texture, 0.75f - 0.0625f), 0x8899bb_rgbf); +} + +void SampleTest::sample1DLinearRgba() { + /* These should return exact values */ + CORRADE_COMPARE(sampleLinear(Texture, 0.0f), 0xff336699_rgbaf); + CORRADE_COMPARE(sampleLinear(Texture, 0.25f), 0xdeadbeef_rgbaf); + CORRADE_COMPARE(sampleLinear(Texture, 1.0f), 0x996633ff_rgbaf); + + /* This is an exact 25% / 75% interpolation between element 2 and 3 */ + CORRADE_COMPARE(sampleLinear(Texture, 0.5f + 0.0625f), 0x4433ddcc_rgbaf); + CORRADE_COMPARE(sampleLinear(Texture, 0.75f - 0.0625f), 0x8899bb66_rgbaf); +} + +void SampleTest::sample1DSrgb() { + Containers::StridedArrayView1D texture = Containers::stridedArrayView(Texture).slice(&Color4ub::rgb); + + /* These should return exact values, converted from sRGB */ + CORRADE_COMPARE(sampleSrgb(texture, 0.0f), 0xff3366_srgbf); + CORRADE_COMPARE(sampleSrgb(texture, 0.25f), 0xdeadbe_srgbf); + CORRADE_COMPARE(sampleSrgb(texture, 1.0f), 0x996633_srgbf); + + /* This is an exact 25% / 75% interpolation between element 2 and 3, but + with sRGB conversion happening first */ + CORRADE_COMPARE(sampleSrgb(texture, 0.5f + 0.0625f), Math::lerp(0x2200ee_srgbf, 0xaaccaa_srgbf, 0.25f)); + CORRADE_COMPARE(sampleSrgb(texture, 0.75f - 0.0625f), Math::lerp(0x2200ee_srgbf, 0xaaccaa_srgbf, 0.75f)); +} + +void SampleTest::sample1DSrgbAlpha() { + /* These should return exact values */ + CORRADE_COMPARE(sampleSrgbAlpha(Texture, 0.0f), 0xff336699_srgbaf); + CORRADE_COMPARE(sampleSrgbAlpha(Texture, 0.25f), 0xdeadbeef_srgbaf); + CORRADE_COMPARE(sampleSrgbAlpha(Texture, 1.0f), 0x996633ff_srgbaf); + + /* This is an exact 25% / 75% interpolation between element 2 and 3, but + with sRGB conversion for the RGB channels happening first */ + CORRADE_COMPARE(sampleSrgbAlpha(Texture, 0.5f + 0.0625f), Math::lerp(0x2200eeff_srgbaf, 0xaaccaa33_srgbaf, 0.25f)); + CORRADE_COMPARE(sampleSrgbAlpha(Texture, 0.75f - 0.0625f), Math::lerp(0x2200eeff_srgbaf, 0xaaccaa33_srgbaf, 0.75f)); + /* The literals should handle alpha as linear but verifying it also + separately just in case -- the channel should have the same value as in + the sample1DLinearRgba() test */ + CORRADE_COMPARE(sampleSrgbAlpha(Texture, 0.5f + 0.0625f).a(), + (Math::unpack(0xcc))); + CORRADE_COMPARE(sampleSrgbAlpha(Texture, 0.75f - 0.0625f).a(), + (Math::unpack(0x66))); +} + +void SampleTest::sample1DInvalid() { + CORRADE_SKIP_IF_NO_ASSERT(); + + Color3ub rgb[1]; + Color4ub rgba[1]; + + Containers::String out; + Error redirectError{&out}; + sampleLinear(Containers::ArrayView{}, 0.0f); + sampleLinear(Containers::ArrayView{}, 0.0f); + sampleSrgb(Containers::ArrayView{}, 0.0f); + sampleSrgbAlpha(Containers::ArrayView{}, 0.0f); + + sampleLinear(rgb, -0.125f); + sampleLinear(rgba, 1.125f); + sampleSrgb(rgb, -Constants::inf()); + sampleSrgbAlpha(rgba, Constants::nan()); + + /* MSVC (w/o clang-cl) before 2019 shows -nan(ind) */ + #if defined(CORRADE_TARGET_MSVC) && !defined(CORRADE_TARGET_CLANG_CL) && _MSC_VER < 1920 + const char* nan = "-nan(ind)"; + #else + const char* nan = "nan"; + #endif + CORRADE_COMPARE_AS(out, Utility::format( + "TextureTools::sampleLinear(): expected texture to have at least one element\n" + "TextureTools::sampleLinear(): expected texture to have at least one element\n" + "TextureTools::sampleSrgb(): expected texture to have at least one element\n" + "TextureTools::sampleSrgbAlpha(): expected texture to have at least one element\n" + + "TextureTools::sampleLinear(): expected factor to be within the [0, 1] range but got -0.125\n" + "TextureTools::sampleLinear(): expected factor to be within the [0, 1] range but got 1.125\n" + "TextureTools::sampleSrgb(): expected factor to be within the [0, 1] range but got -inf\n" + "TextureTools::sampleSrgbAlpha(): expected factor to be within the [0, 1] range but got {}\n", nan), + TestSuite::Compare::String); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::TextureTools::Test::SampleTest)