From 2b987499e205f33f2470376b03ad8f50a9fa939e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Thu, 3 Sep 2020 16:33:32 +0200 Subject: [PATCH] Trade: add attenuation, range and cone angle parameters to LightData. And document how all those go together. --- doc/changelog.dox | 6 + doc/snippets/MagnumTrade.cpp | 25 ++ src/Magnum/Trade/CMakeLists.txt | 2 +- src/Magnum/Trade/LightData.cpp | 36 ++- src/Magnum/Trade/LightData.h | 393 +++++++++++++++++++++++- src/Magnum/Trade/Test/CMakeLists.txt | 2 +- src/Magnum/Trade/Test/LightDataTest.cpp | 364 ++++++++++++++++++++-- 7 files changed, 781 insertions(+), 47 deletions(-) diff --git a/doc/changelog.dox b/doc/changelog.dox index 5253a7987..583e11322 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -99,6 +99,8 @@ See also: - New @ref Trade::SkinData class and @ref Trade::AbstractImporter::skin2D() / @ref Trade::AbstractImporter::skin3D() family of APIs for skin import, as well as support in @ref Trade::AnySceneImporter "AnySceneImporter" +- @ref Trade::LightData got extended to support light attenuation and range + parameters as well and spot light inner and outer angle @subsection changelog-latest-changes Changes and improvements @@ -283,6 +285,10 @@ See also: interfaces, which are also @cpp const @ce and can't fail. Documentation of each function was expanded to suggest a recommended place for potential error handling. +- @cpp Trade::LightData::Type::Infinite @ce, originally adapted from the + OpenGEX specification, is deprecated in favor of + @ref Trade::LightData::Type::Directional as that's the more commonly used + term @section changelog-2020-06 2020.06 diff --git a/doc/snippets/MagnumTrade.cpp b/doc/snippets/MagnumTrade.cpp index 826e844d5..a39cae32f 100644 --- a/doc/snippets/MagnumTrade.cpp +++ b/doc/snippets/MagnumTrade.cpp @@ -40,6 +40,7 @@ #include "Magnum/Trade/AbstractImporter.h" #include "Magnum/Trade/AnimationData.h" #include "Magnum/Trade/ImageData.h" +#include "Magnum/Trade/LightData.h" #include "Magnum/Trade/MaterialData.h" #include "Magnum/Trade/MeshData.h" #include "Magnum/Trade/ObjectData2D.h" @@ -254,6 +255,30 @@ for(auto&& row: data.mutablePixels()) /* [ImageData-usage-mutable] */ } +{ +/* [LightData-populating-range] */ +Trade::LightData data{Trade::LightData::Type::Point, + 0xfff3d6_srgbf, 1.0f, + 15.0f}; +/* [LightData-populating-range] */ +} + +{ +/* [LightData-populating-attenuation] */ +Trade::LightData data{Trade::LightData::Type::Spot, + 0xf3d6ff_srgbf, 10.0f, + {0.01f, 0.5f, 2.0f}, + 25.0_degf, 55.0_degf}; +/* [LightData-populating-attenuation] */ +} + +{ +/* [LightData-populating-none] */ +Trade::LightData data{Trade::LightData::Type::Directional, + 0xd6fff3_srgbf, 0.25f}; +/* [LightData-populating-none] */ +} + { /* [MaterialAttributeData-name] */ Trade::MaterialAttributeData a{ diff --git a/src/Magnum/Trade/CMakeLists.txt b/src/Magnum/Trade/CMakeLists.txt index 61c7b821d..aef7538c7 100644 --- a/src/Magnum/Trade/CMakeLists.txt +++ b/src/Magnum/Trade/CMakeLists.txt @@ -28,7 +28,6 @@ find_package(Corrade REQUIRED PluginManager) set(MagnumTrade_SRCS ArrayAllocator.cpp Data.cpp - LightData.cpp MeshObjectData2D.cpp MeshObjectData3D.cpp SceneData.cpp @@ -42,6 +41,7 @@ set(MagnumTrade_GracefulAssert_SRCS CameraData.cpp FlatMaterialData.cpp ImageData.cpp + LightData.cpp MaterialData.cpp MeshData.cpp ObjectData2D.cpp diff --git a/src/Magnum/Trade/LightData.cpp b/src/Magnum/Trade/LightData.cpp index 9829fe8ac..298915c8c 100644 --- a/src/Magnum/Trade/LightData.cpp +++ b/src/Magnum/Trade/LightData.cpp @@ -27,6 +27,40 @@ namespace Magnum { namespace Trade { +using namespace Math::Literals; + +LightData::LightData(const Type type, const Color3& color, const Float intensity, const Vector3& attenuation, const Float range, const Rad innerConeAngle, const Rad outerConeAngle, const void* const importerState) noexcept: _type{type}, _color{color}, _intensity{intensity}, _attenuation{attenuation}, _range{range}, _innerConeAngle{innerConeAngle}, _outerConeAngle{outerConeAngle}, _importerState{importerState} { + CORRADE_ASSERT(_type != Type::Spot || (Deg(_innerConeAngle) >= 0.0_degf && _innerConeAngle <= _outerConeAngle && Deg(_outerConeAngle) <= 360.0_degf), + "Trade::LightData: spot light inner and outer cone angles have to be in range [0°, 360°] and inner not larger than outer but got" << Deg(_innerConeAngle) << "and" << Deg(_outerConeAngle), ); + CORRADE_ASSERT(_type == Type::Spot || (Math::equal(Deg(_innerConeAngle),360.0_degf) && Math::equal(Deg(_outerConeAngle), 360.0_degf)), + "Trade::LightData: cone angles have to be 360° for lights that aren't spot but got" << Deg(_innerConeAngle) << "and" << Deg(_outerConeAngle), ); + CORRADE_ASSERT(_type != Type::Directional || (_attenuation == Vector3{1.0f, 0.0f, 0.0f}), + "Trade::LightData: attenuation has to be (1, 0, 0) for a directional light but got" << _attenuation, ); + CORRADE_ASSERT(_type != Type::Directional || _range == Constants::inf(), + "Trade::LightData: range has to be infinity for a directional light but got" << _range, ); +} + +LightData::LightData(const Type type, const Color3& color, const Float intensity, const Vector3& attenuation, const Float range, const void* const importerState) noexcept: LightData{type, color, intensity, attenuation, range, + type == Type::Spot ? 0.0_degf : 360.0_degf, + type == Type::Spot ? 45.0_degf : 360.0_degf, + importerState} {} + +LightData::LightData(const Type type, const Color3& color, const Float intensity, const Vector3& attenuation, const Rad innerConeAngle, const Rad outerConeAngle, const void* const importerState) noexcept: LightData{type, color, intensity, attenuation, Constants::inf(), innerConeAngle, outerConeAngle, importerState} {} + +LightData::LightData(const Type type, const Color3& color, const Float intensity, const Vector3& attenuation, const void* const importerState) noexcept: LightData{type, color, intensity, attenuation, Constants::inf(), importerState} {} + +LightData::LightData(const Type type, const Color3& color, const Float intensity, const Float range, const Rad innerConeAngle, const Rad outerConeAngle, const void* const importerState) noexcept: LightData{type, color, intensity, + type == Type::Directional ? Vector3{1.0f, 0.0f, 0.0f} : Vector3{0.0f, 0.0f, 1.0f}, + range, innerConeAngle, outerConeAngle, importerState} {} + +LightData::LightData(const Type type, const Color3& color, const Float intensity, const Float range, const void* const importerState) noexcept: LightData{type, color, intensity, + type == Type::Directional ? Vector3{1.0f, 0.0f, 0.0f} : Vector3{0.0f, 0.0f, 1.0f}, + range, importerState} {} + +LightData::LightData(const Type type, const Color3& color, const Float intensity, const Rad innerConeAngle, const Rad outerConeAngle, const void* const importerState) noexcept: LightData{type, color, intensity, Constants::inf(), innerConeAngle, outerConeAngle, importerState} {} + +LightData::LightData(const Type type, const Color3& color, const Float intensity, const void* const importerState) noexcept: LightData{type, color, intensity, Constants::inf(), importerState} {} + #ifndef DOXYGEN_GENERATING_OUTPUT Debug& operator<<(Debug& debug, const LightData::Type value) { debug << "Trade::LightData::Type" << Debug::nospace; @@ -34,7 +68,7 @@ Debug& operator<<(Debug& debug, const LightData::Type value) { switch(value) { /* LCOV_EXCL_START */ #define _c(value) case LightData::Type::value: return debug << "::" #value; - _c(Infinite) + _c(Directional) _c(Point) _c(Spot) #undef _c diff --git a/src/Magnum/Trade/LightData.h b/src/Magnum/Trade/LightData.h index 7399545b4..18f41c9c1 100644 --- a/src/Magnum/Trade/LightData.h +++ b/src/Magnum/Trade/LightData.h @@ -38,9 +38,83 @@ namespace Magnum { namespace Trade { /** @brief Light data +@section Trade-LightData-usage Usage + +The class exposes light parameters in a way that makes sense as a whole, +allowing to reduce branching in application code --- e.g., a light defined by +just its range has the quadratic attenuation factor set to one, with constant +and linear attenuation being zero, or spot cone angles are the full circle +everything except spotlights. + +@section Trade-LightData-populating Populating an instance + +You can choose a constructor overload that matches the subset of input +parameters and let the class set the rest implicitly. For example, a +@ref Type::Point light constructed using a range will have @ref attenuation() +implicitly set to @cpp {0.0f, 0.0f, 1.0f} @ce and cone angles to +@cpp 360.0_degf @ce: + +@snippet MagnumTrade.cpp LightData-populating-range + +Or, a @ref Type::Spot light constructed from a constant / linear / quadratic +attenuation will have @ref range() implicitly set to @ref Constants::inf(): + +@snippet MagnumTrade.cpp LightData-populating-attenuation + +And a @ref Type::Directional light that doesn't attenuate can be constructed +without either, causing @ref attenuation() to be @cpp {1.0f, 0.0f, 0.0f} @ce +and @ref range() @ref Constants::inf(), cancelling out the attenuation equation: + +@snippet MagnumTrade.cpp LightData-populating-none + +@section Trade-LightData-attenuation Attenuation calculation + +To support all common lighting calculations, the class exposes parameters in a +combined equation containing both constant / linear / quadratic attenuation +@f$ \color{m-success} K_c @f$ / @f$ \color{m-success} K_l @f$ / +@f$ \color{m-success} K_q @f$ and a range parameter @f$ \color{m-info} R @f$ +over a distance @f$ d @f$: @f[ + F_{att} = \frac{\operatorname{clamp}(1 - (\frac{d}{\color{m-info} R})^4, 0, 1)^2}{{\color{m-success} K_c} + {\color{m-success} K_l} d + {\color{m-success} K_q} d^2} +@f] + +In most cases you'll have the light data using either one or the other +approach. The classic constant/linear/quadratic equation allows for most +control, but because the attenuated intensity never really reaches zero, it +makes light culling optimizations hard to perform. In this case the +@ref range() is set to @ref Constants::inf(): @f[ + F_{att} = \lim_{{\color{m-info} R} \to \infty} \frac{{\color{m-dim} \operatorname{clamp}(}1 {\color{m-dim}- (\frac{d}{R})^4, 0, 1)^2}}{{\color{m-success} K_c} + {\color{m-success} K_l} d + {\color{m-success} K_q} d^2} = \frac{1}{{\color{m-success} K_c} + {\color{m-success} K_l} d + {\color{m-success} K_q} d^2} +@f] + +The range-based equation approaches zero when @f$ {\color{m-info} R} = d @f$ +and provides a good tradeoff for performance while staying mostly +physically-based. This is modelled after the glTF [KHR_lights_punctual](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual#range-property) +extension, which in turn is based on the [UE4 implementation](https://github.com/KhronosGroup/glTF/pull/1223#issuecomment-387956919). +In this case, @ref attenuation() is set to +@cpp {0.0f, 0.0f, 1.0f} @ce: @f[ + F_{att} = \frac{\operatorname{clamp}(1 - (\frac{d}{\color{m-info} R})^4, 0, 1)^2}{{\color{m-dim} K_c + K_l d + K_q} d^2} = \frac{\operatorname{clamp}(1 - (\frac{d}{\color{m-info} R})^4, 0, 1)^2}{d^2} +@f] + +If @f$ {\color{m-info} R} \to \infty @f$ as well, the equation reduces down to +a simple inverse square: @f[ + F_{att} = \lim_{{\color{m-info} R} \to \infty} \frac{{\color{m-dim} \operatorname{clamp}(} 1 {\color{m-dim} - (\frac{d}{R})^4, 0, 1)^2}}{{\color{m-dim} K_c + K_l d + K_q} d^2} = \frac{1}{d^2} +@f] + +As a special case, a @ref Type::Directional light is defined by +@ref attenuation() set to @cpp {1.0f, 0.0f, 0.0f} @ce and @ref range() to +@ref Constants::inf() --- thus without any attenuation: @f[ + F_{att} = \lim_{{\color{m-info} R} \to \infty} \frac{{\color{m-dim} \operatorname{clamp}(} 1 {\color{m-dim} - (\frac{d}{R})^4, 0, 1)^2}}{{\color{m-success} K_c} {\color{m-dim} + K_l d + K_q d^2}} = 1 +@f] + +@section Trade-LightData-units Units + +To follow physically-based principles in lighting calculation, intensity is +assumed to be in in *candela* (lm/sr) for @ref Type::Point and @ref Type::Spot, +and in *lux* (lm/m2) for @ref Type::Directional. Distance @f$ d @f$ +is in meters. + @see @ref AbstractImporter::light() */ -class LightData { +class MAGNUM_TRADE_EXPORT LightData { public: /** * @brief Light type @@ -49,18 +123,38 @@ class LightData { */ enum class Type: UnsignedByte { /** - * Light at position that is infinitely far away so its rays are - * parallel. The light rays point in a direction of negative Z - * axis. + * Light at a position that is infinitely far away, emitted in a + * direction of negative Z axis. The rotation is inherited from + * absolute object transformation; scale and position don't affect + * the light in any way. Because the light is at infinite distance, + * it's not attenuated in any way. + * @m_since_latest */ - Infinite, + Directional, - /** Point light, radiating in all directions */ + #ifdef MAGNUM_BUILD_DEPRECATED + /** + * Directional light. + * @m_deprecated_since_latest Use @ref Type::Directional instead. + */ + Infinite CORRADE_DEPRECATED_ENUM("use Type::Directional instead") = Directional, + #endif + + /** + * Point light, emitting light in all directions. The position is + * inherited from absolute object transformation; scale and + * rotation don't affect the light in any way. Brightness + * attenuates depending on the @ref range() value. + */ Point, /** - * Spot light, radiating in a limited range of direction. The - * primary direction is negative Z axis. + * Spot light, emitting light in a cone in direction of local + * negative Z axis. The position and rotation is inherited from + * absolute object transformation; scale doesn't affect the light + * in any way. The angle and falloff of the cone is defined using + * @ref innerConeAngle() and @ref outerConeAngle() and brightness + * attenuates depending on the @ref range() value. */ Spot }; @@ -70,9 +164,220 @@ class LightData { * @param type Light type * @param color Light color * @param intensity Light intensity + * @param attenuation Constant, linear and quadratic light + * attenuation factor. Expected to be @cpp {1.0f, 0.0f, 0.0f} @ce + * for a @ref Type::Directional light. + * @param range Light range, after which the intensity is + * considered to be zero. Expected to be @ref Constants::inf() for + * a @ref Type::Directional light. + * @param innerConeAngle Inner cone angle. Expected to be greater + * than or equal to @cpp 0.0_degf @ce and less than or equal to + * @p outerConeAngle for a @ref Type::Spot light, + * @cpp 360.0_degf @ce otherwise. + * @param outerConeAngle Inner cone angle. Expected to be greater + * than or equal to @p innerConeAngle and less than or equal to + * @cpp 360.0_degf @ce for a @ref Type::Spot light, + * @cpp 360.0_degf @ce otherwise. + * @param importerState Importer-specific state + * @m_since_latest + * + * This is a combined constructor including both attenuation and range + * parameters. Use @ref LightData(Type, const Color3&, Float, const Vector3&, Rad, Rad, const void*) + * for light data defined by just attenuation parameters and + * @ref LightData(Type, const Color3&, Float, Float, Rad, Rad, const void*) + * for light data defined by a range alone, and + * @ref LightData(Type, const Color3&, Float, Rad, Rad, const void*) + * for an implicit inverse square attenuation. See + * @ref Trade-LightData-attenuation for more information. + * + * For lights other than spot it may be more convenient to use + * @ref LightData(Type, const Color3&, Float, const Vector3&, Float, const void*) + * and friends instead. + */ + explicit LightData(Type type, const Color3& color, Float intensity, const Vector3& attenuation, Float range, Rad innerConeAngle, Rad outerConeAngle, const void* importerState = nullptr) noexcept; + + /** + * @brief Construct with implicit cone angles + * @param type Light type + * @param color Light color + * @param intensity Light intensity + * @param attenuation Constant, linear and quadratic light + * attenuation factor. Expected to be @cpp {1.0f, 0.0f, 0.0f} @ce + * for a @ref Type::Directional light. + * @param range Light range, after which the intensity is + * considered to be zero. Expected to be @ref Constants::inf() for + * a @ref Type::Directional light. + * @param importerState Importer-specific state + * @m_since_latest + * + * This is a combined constructor including both attenuation and range + * parameters. Use @ref LightData(Type, const Color3&, Float, const Vector3&, const void*) + * for light data defined by just attenuation parameters and + * @ref LightData(Type, const Color3&, Float, Float, const void*) for + * light data defined by a range alone, and + * @ref LightData(Type, const Color3&, Float, const void*) for an + * implicit inverse square attenuation. See + * @ref Trade-LightData-attenuation for more information. + * + * For a @ref Type::Spot light, @ref innerConeAngle() is implicitly set + * to @cpp 0.0_degf @ce and @ref outerConeAngle() to @cpp 45.0_degf @ce, + * and both are @cpp 360.0_degf @ce otherwise. Use + * @ref LightData(Type, const Color3&, Float, const Vector3&, Float, Rad, Rad, const void*) + * in order to specify cone angles as well. + */ + explicit LightData(Type type, const Color3& color, Float intensity, const Vector3& attenuation, Float range, const void* importerState = nullptr) noexcept; + + /** + * @brief Construct attenuation-based light data + * @param type Light type + * @param color Light color + * @param intensity Light intensity + * @param attenuation Constant, linear and quadratic light + * attenuation factor. Expected to be @cpp {1.0f, 0.0f, 0.0f} @ce + * for a @ref Type::Directional light. + * @param innerConeAngle Inner cone angle. Expected to be greater + * than or equal to @cpp 0.0_degf @ce and less than or equal to + * @p outerConeAngle for a @ref Type::Spot light, + * @cpp 360.0_degf @ce otherwise. + * @param outerConeAngle Inner cone angle. Expected to be greater + * than or equal to @p innerConeAngle and less than or equal to + * @cpp 360.0_degf @ce for a @ref Type::Spot light, + * @cpp 360.0_degf @ce otherwise. + * @param importerState Importer-specific state + * @m_since_latest + * + * The @ref range() is implicitly set to @ref Constants::inf(). See + * @ref Trade-LightData-attenuation for more information. + * + * For lights other than spot it may be more convenient to use + * @ref LightData(Type, const Color3&, Float, const Vector3&, const void*) + * instead. + */ + explicit LightData(Type type, const Color3& color, Float intensity, const Vector3& attenuation, Rad innerConeAngle, Rad outerConeAngle, const void* importerState = nullptr) noexcept; + + /** + * @brief Construct attenuation-based light data with implicit cone angles + * @param type Light type + * @param color Light color + * @param intensity Light intensity + * @param attenuation Constant, linear and quadratic light + * attenuation factor. Expected to be @cpp {1.0f, 0.0f, 0.0f} @ce + * for a @ref Type::Directional light. + * @param importerState Importer-specific state + * @m_since_latest + * + * The @ref range() is implicitly set to @ref Constants::inf(). See + * @ref Trade-LightData-attenuation for more information. + * + * For a @ref Type::Spot light, @ref innerConeAngle() is implicitly set + * to @cpp 0.0_degf @ce and @ref outerConeAngle() to @cpp 45.0_degf @ce, + * and both are @cpp 360.0_degf @ce otherwise. Use + * @ref LightData(Type, const Color3&, Float, const Vector3&, Rad, Rad, const void*) + * in order to specify cone angles as well. + */ + explicit LightData(Type type, const Color3& color, Float intensity, const Vector3& attenuation, const void* importerState = nullptr) noexcept; + + /** + * @brief Construct range-based light data + * @param type Light type + * @param color Light color + * @param intensity Light intensity + * @param range Light range, after which the intensity is + * considered to be zero. Expected to be @ref Constants::inf() for + * a @ref Type::Directional light. + * @param innerConeAngle Inner cone angle. Expected to be greater + * than or equal to @cpp 0.0_degf @ce and less than or equal to + * @p outerConeAngle for a @ref Type::Spot light, + * @cpp 360.0_degf @ce otherwise. + * @param outerConeAngle Inner cone angle. Expected to be greater + * than or equal to @p innerConeAngle and less than or equal to + * @cpp 360.0_degf @ce for a @ref Type::Spot light, + * @cpp 360.0_degf @ce otherwise. + * @param importerState Importer-specific state + * @m_since_latest + * + * The @ref attenuation() is implicitly set to @cpp {0.0f, 0.0f, 1.0f} @ce + * for a @ref Type::Point and @ref Type::Spot light and to + * @cpp {1.0f, 0.0f, 0.0f} @ce for a @ref Type::Directional light. See + * @ref Trade-LightData-attenuation for more information. + * + * For lights other than spot it may be more convenient to use + * @ref LightData(Type, const Color3&, Float, Float, const void*) + * instead. + */ + explicit LightData(Type type, const Color3& color, Float intensity, Float range, Rad innerConeAngle, Rad outerConeAngle, const void* importerState = nullptr) noexcept; + + /** + * @brief Construct range-based light data with implicit cone angles + * @param type Light type + * @param color Light color + * @param intensity Light intensity + * @param range Light range, after which the intensity is + * considered to be zero. Expected to be @ref Constants::inf() for + * a @ref Type::Directional light. + * @param importerState Importer-specific state + * @m_since_latest + * + * The @ref attenuation() is implicitly set to @cpp {0.0f, 0.0f, 1.0f} @ce + * for a @ref Type::Point and @ref Type::Spot light and to + * @cpp {1.0f, 0.0f, 0.0f} @ce for a @ref Type::Directional light. See + * @ref Trade-LightData-attenuation for more information. + * + * For a @ref Type::Spot light, @ref innerConeAngle() is implicitly set + * to @cpp 0.0_degf @ce and @ref outerConeAngle() to @cpp 45.0_degf @ce, + * and both are @cpp 360.0_degf @ce otherwise. Use + * @ref LightData(Type, const Color3&, Float, Float, Rad, Rad, const void*) + * in order to specify cone angles as well. + */ + explicit LightData(Type type, const Color3& color, Float intensity, Float range, const void* importerState = nullptr) noexcept; + + /** + * @brief Construct light data with implicit attenuation + * @param type Light type + * @param color Light color + * @param intensity Light intensity + * @param innerConeAngle Inner cone angle. Expected to be greater + * than or equal to @cpp 0.0_degf @ce and less than or equal to + * @p outerConeAngle for a @ref Type::Spot light, + * @cpp 360.0_degf @ce otherwise. + * @param outerConeAngle Inner cone angle. Expected to be greater + * than or equal to @p innerConeAngle and less than or equal to + * @cpp 360.0_degf @ce for a @ref Type::Spot light, + * @cpp 360.0_degf @ce otherwise. + * @param importerState Importer-specific state + * @m_since_latest + * + * The @ref attenuation() is implicitly set to @cpp {0.0f, 0.0f, 1.0f} @ce + * for a @ref Type::Point and @ref Type::Spot light and to + * @cpp {1.0f, 0.0f, 0.0f} @ce for a @ref Type::Directional light; + * @ref range() is always @ref Constants::inf(). See + * @ref Trade-LightData-attenuation for more information. + * + * For lights other than spot it may be more convenient to use + * @ref LightData(Type, const Color3&, Float, const void*) instead. + */ + explicit LightData(Type type, const Color3& color, Float intensity, Rad innerConeAngle, Rad outerConeAngle, const void* importerState = nullptr) noexcept; + + /** + * @brief Construct light data with implicit attenuation and cone angles + * @param type Light type + * @param color Light color + * @param intensity Light intensity * @param importerState Importer-specific state + * + * The @ref attenuation() is implicitly set to @cpp {0.0f, 0.0f, 1.0f} @ce + * for a @ref Type::Point and @ref Type::Spot light and to + * @cpp {1.0f, 0.0f, 0.0f} @ce for a @ref Type::Directional light; + * @ref range() is always @ref Constants::inf(). See + * @ref Trade-LightData-attenuation for more information. + * + * For a @ref Type::Spot light, @ref innerConeAngle() is implicitly set + * to @cpp 0.0_degf @ce and @ref outerConeAngle() to @cpp 45.0_degf @ce, + * and both are @cpp 360.0_degf @ce otherwise. Use + * @ref LightData(Type, const Color3&, Float, Rad, Rad, const void*) in + * order to specify cone angles as well. */ - constexpr explicit LightData(Type type, const Color3& color, Float intensity, const void* importerState = nullptr) noexcept: _type{type}, _color{color}, _intensity{intensity}, _importerState{importerState} {} + explicit LightData(Type type, const Color3& color, Float intensity, const void* importerState = nullptr) noexcept; /** @brief Copying is not allowed */ LightData(const LightData&) = delete; @@ -87,13 +392,72 @@ class LightData { LightData& operator=(LightData&&) noexcept = default; /** @brief Light type */ - constexpr Type type() const { return _type; } + Type type() const { return _type; } /** @brief Light color */ - constexpr Color3 color() const { return _color; } + Color3 color() const { return _color; } + + /** + * @brief Light intensity + * + * Defined in *candela* (lm/sr) for @ref Type::Point and + * @ref Type::Spot, and in *lux* (lm/m2) for + * @ref Type::Directional. + */ + Float intensity() const { return _intensity; } + + /** + * @brief Constant, linear and quadratic light attenuation + * @m_since_latest + * + * Values of @f$ \color{m-success} K_c @f$, + * @f$ \color{m-success} K_l @f$ and @f$ \color{m-success} K_q @f$ in + * the @ref Trade-LightData-attenuation "attenuation equation". Always + * @cpp {1.0f, 0.0f, 0.0f} @ce for a @ref Type::Directional + * light, set to @cpp {0.0f, 0.0f, 1.0f} @ce for range-based + * attenuation --- and if @ref range() is @ref Constants::inf() as + * well, the attenuation equation is simply + * @f$ F_{att} = \frac{1}{d^2} @f$. + */ + Vector3 attenuation() const { return _attenuation; } - /** @brief Light intensity */ - constexpr Float intensity() const { return _intensity; } + /** + * @brief Light range + * @m_since_latest + * + * Value of @f$ \color{m-info} R @f$ in + * the @ref Trade-LightData-attenuation "attenuation equation". If set + * to @ref Constants::inf(), then: + * + * - if @ref attenuation() is @cpp {0.0f, 0.0f, 1.0f} @ce, the + * attenuation equation is @f$ F_{att} = \frac{1}{d^2} @f$; + * - if @ref attenuation() is @cpp {1.0f, 0.0f, 0.0f} @ce, the + * attenuation equation is @f$ F_{att} = 1 @f$. + * + * The latter is always the case for a @ref Type::Directional light. + */ + Float range() const { return _range; } + + /** + * @brief Inner cone angle + * @m_since_latest + * + * For a @ref Type::Spot light, it's always less than + * @ref outerConeAngle(). For a @ref Type::Directional or + * @ref Type::Point light it's always @cpp 360.0_degf @ce. + */ + Rad innerConeAngle() const { return _innerConeAngle; } + + /** + * @brief Outer cone angle + * @m_since_latest + * + * For a @ref Type::Spot light, it's always greater than + * @ref outerConeAngle() and less than or equal to @cpp 90.0_degf @ce. + * For a @ref Type::Directional or @ref Type::Point light it's always + * @cpp 360.0_degf @ce. + */ + Rad outerConeAngle() const { return _outerConeAngle; } /** * @brief Importer-specific state @@ -106,6 +470,9 @@ class LightData { Type _type; Vector3 _color; Float _intensity; + Vector3 _attenuation; + Float _range; + Rad _innerConeAngle, _outerConeAngle; const void* _importerState; }; diff --git a/src/Magnum/Trade/Test/CMakeLists.txt b/src/Magnum/Trade/Test/CMakeLists.txt index a4ae7ecfa..cd88f4a55 100644 --- a/src/Magnum/Trade/Test/CMakeLists.txt +++ b/src/Magnum/Trade/Test/CMakeLists.txt @@ -50,7 +50,7 @@ corrade_add_test(TradeAnimationDataTest AnimationDataTest.cpp LIBRARIES MagnumTr corrade_add_test(TradeCameraDataTest CameraDataTest.cpp LIBRARIES MagnumTradeTestLib) corrade_add_test(TradeDataTest DataTest.cpp LIBRARIES MagnumTrade) corrade_add_test(TradeImageDataTest ImageDataTest.cpp LIBRARIES MagnumTradeTestLib) -corrade_add_test(TradeLightDataTest LightDataTest.cpp LIBRARIES MagnumTrade) +corrade_add_test(TradeLightDataTest LightDataTest.cpp LIBRARIES MagnumTradeTestLib) corrade_add_test(TradeMaterialDataTest MaterialDataTest.cpp LIBRARIES MagnumTradeTestLib) corrade_add_test(TradeMeshDataTest MeshDataTest.cpp LIBRARIES MagnumTradeTestLib) corrade_add_test(TradeObjectData2DTest ObjectData2DTest.cpp LIBRARIES MagnumTradeTestLib) diff --git a/src/Magnum/Trade/Test/LightDataTest.cpp b/src/Magnum/Trade/Test/LightDataTest.cpp index bb3089b41..f471f4c58 100644 --- a/src/Magnum/Trade/Test/LightDataTest.cpp +++ b/src/Magnum/Trade/Test/LightDataTest.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include "Magnum/Trade/LightData.h" @@ -35,62 +36,363 @@ struct LightDataTest: TestSuite::Tester { explicit LightDataTest(); void construct(); + void constructAttenuation(); + void constructRange(); + void constructNone(); + + void constructInvalid(); + void constructCopy(); void constructMove(); void debugType(); }; +using namespace Math::Literals; + +const struct { + const char* name; + LightData::Type type; + Vector3 attenuation; + Float range; + Rad innerConeAngle; + Rad outerConeAngle; + const char* message; +} ConstructInvalidData[] { + {"invalid directional attenuation", LightData::Type::Directional, + {0.0f, 0.0f, 1.0f}, Constants::inf(), 360.0_degf, 360.0_degf, + "attenuation has to be (1, 0, 0) for a directional light but got Vector(0, 0, 1)"}, + {"invalid directional range", LightData::Type::Directional, + {1.0f, 0.0f, 0.0f}, 2.0f, 360.0_degf, 360.0_degf, + "range has to be infinity for a directional light but got 2"}, + {"invalid point angles", LightData::Type::Point, + {0.0f, 0.0f, 1.0f}, Constants::inf(), 15.0_degf, 90.0_degf, + "cone angles have to be 360° for lights that aren't spot but got Deg(15) and Deg(90)"}, + {"negative inner spot angle", LightData::Type::Spot, + {0.0f, 0.0f, 1.0f}, Constants::inf(), -1.0_degf, 90.0_degf, + "spot light inner and outer cone angles have to be in range [0°, 360°] and inner not larger than outer but got Deg(-1) and Deg(90)"}, + {"too big outer spot angle", LightData::Type::Spot, + {0.0f, 0.0f, 1.0f}, Constants::inf(), 0.0_degf, 361.0_degf, + "spot light inner and outer cone angles have to be in range [0°, 360°] and inner not larger than outer but got Deg(0) and Deg(361)"}, + {"inner spot angle larger than outer", LightData::Type::Spot, + {0.0f, 0.0f, 1.0f}, Constants::inf(), 35.0_degf, 30.0_degf, + "spot light inner and outer cone angles have to be in range [0°, 360°] and inner not larger than outer but got Deg(35) and Deg(30)"} +}; + LightDataTest::LightDataTest() { addTests({&LightDataTest::construct, - &LightDataTest::constructCopy, + &LightDataTest::constructAttenuation, + &LightDataTest::constructRange, + &LightDataTest::constructNone}); + + addInstancedTests({&LightDataTest::constructInvalid}, + Containers::arraySize(ConstructInvalidData)); + + addTests({&LightDataTest::constructCopy, &LightDataTest::constructMove, &LightDataTest::debugType}); } void LightDataTest::construct() { - using namespace Math::Literals; - const int a{}; - LightData data{LightData::Type::Infinite, 0xccff33_rgbf, 0.8f, &a}; - - CORRADE_COMPARE(data.type(), LightData::Type::Infinite); - CORRADE_COMPARE(data.color(), 0xccff33_rgbf); - CORRADE_COMPARE(data.intensity(), 0.8f); - CORRADE_COMPARE(data.importerState(), &a); + { + int a; + LightData data{LightData::Type::Spot, + 0xccff33_rgbf, 0.8f, + {0.1f, 0.5f, 0.7f}, 15.0f, + 15.0_degf, 35.0_degf, + &a}; + + CORRADE_COMPARE(data.type(), LightData::Type::Spot); + CORRADE_COMPARE(data.color(), 0xccff33_rgbf); + CORRADE_COMPARE(data.intensity(), 0.8f); + CORRADE_COMPARE(data.attenuation(), (Vector3{0.1f, 0.5f, 0.7f})); + CORRADE_COMPARE(data.range(), 15.0f); + CORRADE_COMPARE(data.innerConeAngle(), 15.0_degf); + CORRADE_COMPARE(data.outerConeAngle(), 35.0_degf); + CORRADE_COMPARE(data.importerState(), &a); + + /* Implicit spot angles */ + } { + int a; + LightData data{LightData::Type::Spot, + 0xccff33_rgbf, 0.8f, + {0.1f, 0.5f, 0.7f}, 15.0f, + &a}; + + CORRADE_COMPARE(data.type(), LightData::Type::Spot); + CORRADE_COMPARE(data.color(), 0xccff33_rgbf); + CORRADE_COMPARE(data.intensity(), 0.8f); + CORRADE_COMPARE(data.attenuation(), (Vector3{0.1f, 0.5f, 0.7f})); + CORRADE_COMPARE(data.range(), 15.0f); + CORRADE_COMPARE(data.innerConeAngle(), 0.0_degf); + CORRADE_COMPARE(data.outerConeAngle(), 45.0_degf); + CORRADE_COMPARE(data.importerState(), &a); + + /* Implicit non-spot angles */ + } { + int a; + LightData data{LightData::Type::Point, + 0xccff33_rgbf, 0.8f, + {0.1f, 0.5f, 0.7f}, 15.0f, + &a}; + + CORRADE_COMPARE(data.type(), LightData::Type::Point); + CORRADE_COMPARE(data.color(), 0xccff33_rgbf); + CORRADE_COMPARE(data.intensity(), 0.8f); + CORRADE_COMPARE(data.attenuation(), (Vector3{0.1f, 0.5f, 0.7f})); + CORRADE_COMPARE(data.range(), 15.0f); + CORRADE_COMPARE(data.innerConeAngle(), 360.0_degf); + CORRADE_COMPARE(data.outerConeAngle(), 360.0_degf); + CORRADE_COMPARE(data.importerState(), &a); + } +} + +void LightDataTest::constructAttenuation() { + /* Implicit range */ + { + int a; + LightData data{LightData::Type::Spot, + 0xccff33_rgbf, 0.8f, + {0.1f, 0.5f, 0.7f}, + 15.0_degf, 35.0_degf, + &a}; + + CORRADE_COMPARE(data.type(), LightData::Type::Spot); + CORRADE_COMPARE(data.color(), 0xccff33_rgbf); + CORRADE_COMPARE(data.intensity(), 0.8f); + CORRADE_COMPARE(data.attenuation(), (Vector3{0.1f, 0.5f, 0.7f})); + CORRADE_COMPARE(data.range(), Constants::inf()); + CORRADE_COMPARE(data.innerConeAngle(), 15.0_degf); + CORRADE_COMPARE(data.outerConeAngle(), 35.0_degf); + CORRADE_COMPARE(data.importerState(), &a); + + /* Implicit range + spot angles */ + } { + int a; + LightData data{LightData::Type::Spot, + 0xccff33_rgbf, 0.8f, + {0.1f, 0.5f, 0.7f}, + &a}; + + CORRADE_COMPARE(data.type(), LightData::Type::Spot); + CORRADE_COMPARE(data.color(), 0xccff33_rgbf); + CORRADE_COMPARE(data.intensity(), 0.8f); + CORRADE_COMPARE(data.attenuation(), (Vector3{0.1f, 0.5f, 0.7f})); + CORRADE_COMPARE(data.range(), Constants::inf()); + CORRADE_COMPARE(data.innerConeAngle(), 0.0_degf); + CORRADE_COMPARE(data.outerConeAngle(), 45.0_degf); + CORRADE_COMPARE(data.importerState(), &a); + + /* Implicit range + non-spot angles */ + } { + int a; + LightData data{LightData::Type::Point, + 0xccff33_rgbf, 0.8f, + {0.1f, 0.5f, 0.7f}, + &a}; + + CORRADE_COMPARE(data.type(), LightData::Type::Point); + CORRADE_COMPARE(data.color(), 0xccff33_rgbf); + CORRADE_COMPARE(data.intensity(), 0.8f); + CORRADE_COMPARE(data.attenuation(), (Vector3{0.1f, 0.5f, 0.7f})); + CORRADE_COMPARE(data.range(), Constants::inf()); + CORRADE_COMPARE(data.innerConeAngle(), 360.0_degf); + CORRADE_COMPARE(data.outerConeAngle(), 360.0_degf); + CORRADE_COMPARE(data.importerState(), &a); + } +} + +void LightDataTest::constructRange() { + /* Implicit attenuation for a spot */ + { + int a; + LightData data{LightData::Type::Spot, + 0xccff33_rgbf, 0.8f, + 15.0f, + 15.0_degf, 35.0_degf, + &a}; + + CORRADE_COMPARE(data.type(), LightData::Type::Spot); + CORRADE_COMPARE(data.color(), 0xccff33_rgbf); + CORRADE_COMPARE(data.intensity(), 0.8f); + CORRADE_COMPARE(data.attenuation(), (Vector3{0.0f, 0.0f, 1.0f})); + CORRADE_COMPARE(data.range(), 15.0f); + CORRADE_COMPARE(data.innerConeAngle(), 15.0_degf); + CORRADE_COMPARE(data.outerConeAngle(), 35.0_degf); + CORRADE_COMPARE(data.importerState(), &a); + + /* Implicit attenuation for a spot + spot angles */ + } { + int a; + LightData data{LightData::Type::Spot, + 0xccff33_rgbf, 0.8f, + 15.0f, + &a}; + + CORRADE_COMPARE(data.type(), LightData::Type::Spot); + CORRADE_COMPARE(data.color(), 0xccff33_rgbf); + CORRADE_COMPARE(data.intensity(), 0.8f); + CORRADE_COMPARE(data.attenuation(), (Vector3{0.0f, 0.0f, 1.0f})); + CORRADE_COMPARE(data.range(), 15.0f); + CORRADE_COMPARE(data.innerConeAngle(), 0.0_degf); + CORRADE_COMPARE(data.outerConeAngle(), 45.0_degf); + CORRADE_COMPARE(data.importerState(), &a); + + /* Implicit attenuation for a point + non-spot angles */ + } { + int a; + LightData data{LightData::Type::Point, + 0xccff33_rgbf, 0.8f, + 15.0f, + &a}; + + CORRADE_COMPARE(data.type(), LightData::Type::Point); + CORRADE_COMPARE(data.color(), 0xccff33_rgbf); + CORRADE_COMPARE(data.intensity(), 0.8f); + CORRADE_COMPARE(data.attenuation(), (Vector3{0.0f, 0.0f, 1.0f})); + CORRADE_COMPARE(data.range(), 15.0f); + CORRADE_COMPARE(data.innerConeAngle(), 360.0_degf); + CORRADE_COMPARE(data.outerConeAngle(), 360.0_degf); + CORRADE_COMPARE(data.importerState(), &a); + + /* Implicit attenuation for a directional + non-spot angles */ + } { + int a; + LightData data{LightData::Type::Directional, + 0xccff33_rgbf, 0.8f, + Constants::inf(), + &a}; + + CORRADE_COMPARE(data.type(), LightData::Type::Directional); + CORRADE_COMPARE(data.color(), 0xccff33_rgbf); + CORRADE_COMPARE(data.intensity(), 0.8f); + CORRADE_COMPARE(data.attenuation(), (Vector3{1.0f, 0.0f, 0.0f})); + CORRADE_COMPARE(data.range(), Constants::inf()); + CORRADE_COMPARE(data.innerConeAngle(), 360.0_degf); + CORRADE_COMPARE(data.outerConeAngle(), 360.0_degf); + CORRADE_COMPARE(data.importerState(), &a); + } +} + +void LightDataTest::constructNone() { + /* Implicit attenuation + range for a spot */ + { + int a; + LightData data{LightData::Type::Spot, + 0xccff33_rgbf, 0.8f, + 15.0_degf, 35.0_degf, + &a}; + + CORRADE_COMPARE(data.type(), LightData::Type::Spot); + CORRADE_COMPARE(data.color(), 0xccff33_rgbf); + CORRADE_COMPARE(data.intensity(), 0.8f); + CORRADE_COMPARE(data.attenuation(), (Vector3{0.0f, 0.0f, 1.0f})); + CORRADE_COMPARE(data.range(), Constants::inf()); + CORRADE_COMPARE(data.innerConeAngle(), 15.0_degf); + CORRADE_COMPARE(data.outerConeAngle(), 35.0_degf); + CORRADE_COMPARE(data.importerState(), &a); + + /* Implicit attenuation + range for a spot + spot angles */ + } { + int a; + LightData data{LightData::Type::Spot, + 0xccff33_rgbf, 0.8f, + &a}; + + CORRADE_COMPARE(data.type(), LightData::Type::Spot); + CORRADE_COMPARE(data.color(), 0xccff33_rgbf); + CORRADE_COMPARE(data.intensity(), 0.8f); + CORRADE_COMPARE(data.attenuation(), (Vector3{0.0f, 0.0f, 1.0f})); + CORRADE_COMPARE(data.range(), Constants::inf()); + CORRADE_COMPARE(data.innerConeAngle(), 0.0_degf); + CORRADE_COMPARE(data.outerConeAngle(), 45.0_degf); + CORRADE_COMPARE(data.importerState(), &a); + + /* Implicit attenuation + range for a point + non-spot angles */ + } { + int a; + LightData data{LightData::Type::Point, + 0xccff33_rgbf, 0.8f, + &a}; + + CORRADE_COMPARE(data.type(), LightData::Type::Point); + CORRADE_COMPARE(data.color(), 0xccff33_rgbf); + CORRADE_COMPARE(data.intensity(), 0.8f); + CORRADE_COMPARE(data.attenuation(), (Vector3{0.0f, 0.0f, 1.0f})); + CORRADE_COMPARE(data.range(), Constants::inf()); + CORRADE_COMPARE(data.innerConeAngle(), 360.0_degf); + CORRADE_COMPARE(data.outerConeAngle(), 360.0_degf); + CORRADE_COMPARE(data.importerState(), &a); + + /* Implicit attenuation for a directional + non-spot angles */ + } { + int a; + LightData data{LightData::Type::Directional, + 0xccff33_rgbf, 0.8f, + &a}; + + CORRADE_COMPARE(data.type(), LightData::Type::Directional); + CORRADE_COMPARE(data.color(), 0xccff33_rgbf); + CORRADE_COMPARE(data.intensity(), 0.8f); + CORRADE_COMPARE(data.attenuation(), (Vector3{1.0f, 0.0f, 0.0f})); + CORRADE_COMPARE(data.range(), Constants::inf()); + CORRADE_COMPARE(data.innerConeAngle(), 360.0_degf); + CORRADE_COMPARE(data.outerConeAngle(), 360.0_degf); + CORRADE_COMPARE(data.importerState(), &a); + } +} + +void LightDataTest::constructInvalid() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + auto&& data = ConstructInvalidData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + std::ostringstream out; + Error redirectError{&out}; + LightData{data.type, {}, {}, data.attenuation, data.range, data.innerConeAngle, data.outerConeAngle}; + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::LightData: {}\n", data.message)); } void LightDataTest::constructCopy() { CORRADE_VERIFY(!(std::is_constructible{})); CORRADE_VERIFY(!(std::is_assignable{})); - - CORRADE_VERIFY(std::is_nothrow_move_constructible::value); - CORRADE_VERIFY(std::is_nothrow_move_assignable::value); } void LightDataTest::constructMove() { - using namespace Math::Literals; - const int a{}; - LightData data{LightData::Type::Infinite, 0xccff33_rgbf, 0.8f, &a}; - - CORRADE_COMPARE(data.type(), LightData::Type::Infinite); - CORRADE_COMPARE(data.color(), 0xccff33_rgbf); - CORRADE_COMPARE(data.intensity(), 0.8f); - CORRADE_COMPARE(data.importerState(), &a); + int state; + LightData a{LightData::Type::Spot, + 0xccff33_rgbf, 0.8f, + {0.1f, 0.5f, 0.7f}, 15.0f, + 15.0_degf, 35.0_degf, + &state}; - LightData b{std::move(data)}; - CORRADE_COMPARE(b.type(), LightData::Type::Infinite); + LightData b{std::move(a)}; + CORRADE_COMPARE(b.type(), LightData::Type::Spot); CORRADE_COMPARE(b.color(), 0xccff33_rgbf); CORRADE_COMPARE(b.intensity(), 0.8f); - CORRADE_COMPARE(b.importerState(), &a); - - const int c{}; - LightData d{LightData::Type::Point, 0xdead00_rgbf, 1.6f, &c}; - d = std::move(b); - CORRADE_COMPARE(d.type(), LightData::Type::Infinite); - CORRADE_COMPARE(d.color(), 0xccff33_rgbf); - CORRADE_COMPARE(d.intensity(), 0.8f); - CORRADE_COMPARE(d.importerState(), &a); + CORRADE_COMPARE(b.attenuation(), (Vector3{0.1f, 0.5f, 0.7f})); + CORRADE_COMPARE(b.range(), 15.0f); + CORRADE_COMPARE(b.innerConeAngle(), 15.0_degf); + CORRADE_COMPARE(b.outerConeAngle(), 35.0_degf); + CORRADE_COMPARE(b.importerState(), &state); + + LightData c{{}, {}, {}}; + c = std::move(a); + CORRADE_COMPARE(c.type(), LightData::Type::Spot); + CORRADE_COMPARE(c.color(), 0xccff33_rgbf); + CORRADE_COMPARE(c.intensity(), 0.8f); + CORRADE_COMPARE(c.attenuation(), (Vector3{0.1f, 0.5f, 0.7f})); + CORRADE_COMPARE(c.range(), 15.0f); + CORRADE_COMPARE(c.innerConeAngle(), 15.0_degf); + CORRADE_COMPARE(c.outerConeAngle(), 35.0_degf); + CORRADE_COMPARE(c.importerState(), &state); + + CORRADE_VERIFY(std::is_nothrow_move_constructible::value); + CORRADE_VERIFY(std::is_nothrow_move_assignable::value); } void LightDataTest::debugType() {