From 567b15486fa032bee3b0b15b522205fc960d9577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sun, 13 Sep 2020 13:38:16 +0200 Subject: [PATCH] Shaders: add an ability to control specular light color to Phong. --- doc/changelog.dox | 2 + doc/snippets/MagnumShaders.cpp | 1 + src/Magnum/Shaders/Phong.cpp | 29 +++++++- src/Magnum/Shaders/Phong.frag | 14 +++- src/Magnum/Shaders/Phong.h | 44 ++++++++++-- src/Magnum/Shaders/Test/CMakeLists.txt | 1 + src/Magnum/Shaders/Test/PhongGLTest.cpp | 65 +++++++++++++----- .../PhongTestFiles/light-point-range1.5.tga | Bin 6810 -> 6810 bytes .../light-point-specular-color.tga | Bin 0 -> 8130 bytes 9 files changed, 130 insertions(+), 26 deletions(-) create mode 100644 src/Magnum/Shaders/Test/PhongTestFiles/light-point-specular-color.tga diff --git a/doc/changelog.dox b/doc/changelog.dox index 502ea4978..0083b0ff5 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -83,6 +83,8 @@ See also: - @ref Shaders::Phong was reworked to support directional and range-attenuated point lights to follow the additions to @ref Trade::LightData +- Added @ref Shaders::Phong::setLightSpecularColors() for better control over + speculat highlights @subsubsection changelog-latest-new-scenegraph SceneGraph library diff --git a/doc/snippets/MagnumShaders.cpp b/doc/snippets/MagnumShaders.cpp index 75211da65..048a03cd9 100644 --- a/doc/snippets/MagnumShaders.cpp +++ b/doc/snippets/MagnumShaders.cpp @@ -546,6 +546,7 @@ shader .setLightColors({0xf0f0ff_srgbf*0.1f, 0xff8080_srgbf*10.0f, 0x80ff80_srgbf*10.0f}) + .setLightColors(DOXYGEN_IGNORE({0xf0f0ff_srgbf})) .setLightRanges({Constants::inf(), 2.0f, 2.0f}); diff --git a/src/Magnum/Shaders/Phong.cpp b/src/Magnum/Shaders/Phong.cpp index 0e7e444d4..42b52efc8 100644 --- a/src/Magnum/Shaders/Phong.cpp +++ b/src/Magnum/Shaders/Phong.cpp @@ -55,7 +55,7 @@ namespace { }; } -Phong::Phong(const Flags flags, const UnsignedInt lightCount): _flags{flags}, _lightCount{lightCount}, _lightColorsUniform{_lightPositionsUniform + Int(lightCount)}, _lightRangesUniform{_lightPositionsUniform + 2*Int(lightCount)} { +Phong::Phong(const Flags flags, const UnsignedInt lightCount): _flags{flags}, _lightCount{lightCount}, _lightColorsUniform{_lightPositionsUniform + Int(lightCount)}, _lightSpecularColorsUniform{_lightPositionsUniform + 2*Int(lightCount)}, _lightRangesUniform{_lightPositionsUniform + 3*Int(lightCount)} { CORRADE_ASSERT(!(flags & Flag::TextureTransformation) || (flags & (Flag::AmbientTexture|Flag::DiffuseTexture|Flag::SpecularTexture|Flag::NormalTexture)), "Shaders::Phong: texture transformation enabled but the shader is not textured", ); @@ -156,10 +156,12 @@ Phong::Phong(const Flags flags, const UnsignedInt lightCount): _flags{flags}, _l .addSource(Utility::formatString( "#define LIGHT_COUNT {}\n" "#define LIGHT_COLORS_LOCATION {}\n" + "#define LIGHT_SPECULAR_COLORS_LOCATION {}\n" "#define LIGHT_RANGES_LOCATION {}\n", lightCount, _lightPositionsUniform + lightCount, - _lightPositionsUniform + 2*lightCount)); + _lightPositionsUniform + 2*lightCount, + _lightPositionsUniform + 3*lightCount)); #ifndef MAGNUM_TARGET_GLES if(lightCount) frag.addSource(std::move(lightInitializerFragment)); #endif @@ -224,6 +226,7 @@ Phong::Phong(const Flags flags, const UnsignedInt lightCount): _flags{flags}, _l _normalTextureScaleUniform = uniformLocation("normalTextureScale"); _lightPositionsUniform = uniformLocation("lightPositions"); _lightColorsUniform = uniformLocation("lightColors"); + _lightSpecularColorsUniform = uniformLocation("lightSpecularColors"); _lightRangesUniform = uniformLocation("lightRanges"); } if(flags & Flag::AlphaMask) _alphaMaskUniform = uniformLocation("alphaMask"); @@ -258,7 +261,9 @@ Phong::Phong(const Flags flags, const UnsignedInt lightCount): _flags{flags}, _l if(flags & Flag::NormalTexture) setNormalTextureScale(1.0f); setLightPositions(Containers::Array{Containers::DirectInit, lightCount, Vector4{0.0f, 0.0f, 1.0f, 0.0f}}); - setLightColors(Containers::Array{Containers::DirectInit, lightCount, Magnum::Color3{1.0f}}); + Containers::Array colors{Containers::DirectInit, lightCount, Magnum::Color3{1.0f}}; + setLightColors(colors); + setLightSpecularColors(colors); setLightRanges(Containers::Array{Containers::DirectInit, lightCount, Constants::inf()}); /* Light position is zero by default */ setNormalMatrix({}); @@ -461,6 +466,24 @@ Phong& Phong::setLightColor(const Magnum::Color4& color) { } #endif +Phong& Phong::setLightSpecularColors(const Containers::ArrayView colors) { + CORRADE_ASSERT(_lightCount == colors.size(), + "Shaders::Phong::setLightSpecularColors(): expected" << _lightCount << "items but got" << colors.size(), *this); + if(_lightCount) setUniform(_lightSpecularColorsUniform, colors); + return *this; +} + +Phong& Phong::setLightSpecularColors(const std::initializer_list colors) { + return setLightSpecularColors(Containers::arrayView(colors)); +} + +Phong& Phong::setLightSpecularColor(const UnsignedInt id, const Magnum::Color3& color) { + CORRADE_ASSERT(id < _lightCount, + "Shaders::Phong::setLightSpecularColor(): light ID" << id << "is out of bounds for" << _lightCount << "lights", *this); + setUniform(_lightSpecularColorsUniform + id, color); + return *this; +} + Phong& Phong::setLightRanges(const Containers::ArrayView ranges) { CORRADE_ASSERT(_lightCount == ranges.size(), "Shaders::Phong::setLightRanges(): expected" << _lightCount << "items but got" << ranges.size(), *this); diff --git a/src/Magnum/Shaders/Phong.frag b/src/Magnum/Shaders/Phong.frag index c8fa162a1..46109b924 100644 --- a/src/Magnum/Shaders/Phong.frag +++ b/src/Magnum/Shaders/Phong.frag @@ -140,7 +140,8 @@ uniform highp uint objectId; /* defaults to zero */ #if LIGHT_COUNT /* Needs to be last because it uses locations 11 + LIGHT_COUNT to 11 + 2*LIGHT_COUNT - 1. Location 11 is lightPositions. Also it can't be - specified as 11 + LIGHT_COUNT because that requires ARB_enhanced_layouts. */ + specified as 11 + LIGHT_COUNT because that requires ARB_enhanced_layouts. + Same for lightSpecularColors and lightRanges below. */ #ifdef EXPLICIT_UNIFORM_LOCATION layout(location = LIGHT_COLORS_LOCATION) /* I fear this will blow up some drivers */ #endif @@ -150,6 +151,15 @@ uniform lowp vec3 lightColors[LIGHT_COUNT] #endif ; +#ifdef EXPLICIT_UNIFORM_LOCATION +layout(location = LIGHT_SPECULAR_COLORS_LOCATION) +#endif +uniform lowp vec3 lightSpecularColors[LIGHT_COUNT] + #ifndef GL_ES + = vec3[](LIGHT_COLOR_INITIALIZER) + #endif + ; + #ifdef EXPLICIT_UNIFORM_LOCATION layout(location = LIGHT_RANGES_LOCATION) #endif @@ -272,7 +282,7 @@ void main() { highp vec3 reflection = reflect(-normalizedLightDirection, normalizedTransformedNormal); /* Use attenuation for the specularity as well */ mediump float specularity = clamp(pow(max(0.0, dot(normalize(cameraDirection), reflection)), shininess), 0.0, 1.0)*attenuation; - fragmentColor += vec4(finalSpecularColor.rgb*specularity, finalSpecularColor.a); + fragmentColor += vec4(finalSpecularColor.rgb*lightSpecularColors[i].rgb*specularity, finalSpecularColor.a); } } #endif diff --git a/src/Magnum/Shaders/Phong.h b/src/Magnum/Shaders/Phong.h index 77bb29cbb..f0501a6c7 100644 --- a/src/Magnum/Shaders/Phong.h +++ b/src/Magnum/Shaders/Phong.h @@ -78,9 +78,10 @@ Common rendering setup: By default, the shader provides a single directional "fill" light, coming from the center of the camera. Using the @p lightCount parameter in constructor, you can specify how many lights you want, and then control light parameters using -the following @ref setLightPositions(), @ref setLightColors() and -@ref setLightRanges(). Light positions are specified as four-component vectors, -the last component distinguishing between directional and point lights. +@ref setLightPositions(), @ref setLightColors(), @ref setLightSpecularColors() +and @ref setLightRanges(). Light positions are specified as four-component +vectors, the last component distinguishing between directional and point +lights.
  • Point lights are specified with camera-relative position and the last component @@ -106,7 +107,7 @@ any way: @f[ Light color and intensity, corresponding to @ref Trade::LightData::color() and @ref Trade::LightData::intensity(), is meant to be multiplied together and -passed to @ref setLightColors(). +passed to @ref setLightColors() and @ref setLightSpecularColors(). The following example shows a three-light setup with one dim directional light shining from the top and two stronger but range-limited point lights: @@ -941,6 +942,38 @@ class MAGNUM_SHADERS_EXPORT Phong: public GL::AbstractShaderProgram { CORRADE_DEPRECATED("use setLightColor(std::initializer_list) instead") Phong& setLightColor(const Magnum::Color4& color); #endif + /** + * @brief Set light specular colors + * @return Reference to self (for method chaining) + * @m_since_latest + * + * Usually you'd set this value to the same as @ref setLightColors(), + * but it allows for greater flexibility such as disabling specular + * highlights on certain lights. Initial values are + * @cpp 0xffffff_rgbf @ce. Expects that the size of the @p colors array + * is the same as @ref lightCount(). + * @see @ref Shaders-Phong-lights, @ref setLightColor() + */ + Phong& setLightSpecularColors(Containers::ArrayView colors); + + /** + * @overload + * @m_since_latest + */ + Phong& setLightSpecularColors(std::initializer_list colors); + + /** + * @brief Set position for given light + * @return Reference to self (for method chaining) + * @m_since_latest + * + * Unlike @ref setLightSpecularColors() updates just a single light + * color. If updating more than one light, prefer the batch function + * instead to reduce the count of GL API calls. Expects that @p id is + * less than @ref lightCount(). + */ + Phong& setLightSpecularColor(UnsignedInt id, const Magnum::Color3& color); + /** * @brief Set light attenuation ranges * @return Reference to self (for method chaining) @@ -996,7 +1029,8 @@ class MAGNUM_SHADERS_EXPORT Phong: public GL::AbstractShaderProgram { #endif Int _lightPositionsUniform{11}, _lightColorsUniform, /* 11 + lightCount, set in the constructor */ - _lightRangesUniform; /* 11 + 2*lightCount, set in the constructor */ + _lightSpecularColorsUniform, /* 11 + 2*lightCount */ + _lightRangesUniform; /* 11 + 3*lightCount */ }; /** @debugoperatorclassenum{Phong,Phong::Flag} */ diff --git a/src/Magnum/Shaders/Test/CMakeLists.txt b/src/Magnum/Shaders/Test/CMakeLists.txt index 6850dfff6..d1ae4b61d 100644 --- a/src/Magnum/Shaders/Test/CMakeLists.txt +++ b/src/Magnum/Shaders/Test/CMakeLists.txt @@ -246,6 +246,7 @@ if(BUILD_GL_TESTS) PhongTestFiles/light-point-attenuated-specular.tga PhongTestFiles/light-point-intensity10-range0.5.tga PhongTestFiles/light-point-range1.5.tga + PhongTestFiles/light-point-specular-color.tga PhongTestFiles/light-point.tga # For zero lights test (equivalency to Flat3D) diff --git a/src/Magnum/Shaders/Test/PhongGLTest.cpp b/src/Magnum/Shaders/Test/PhongGLTest.cpp index 6472745cf..4c2b16cae 100644 --- a/src/Magnum/Shaders/Test/PhongGLTest.cpp +++ b/src/Magnum/Shaders/Test/PhongGLTest.cpp @@ -344,12 +344,14 @@ const struct { const char* name; const char* file; Vector4 position; + Color3 specularColor, lightSpecularColor; Float intensity; Float range; Containers::Array> picks; } RenderLightsData[] { {"directional", "light-directional.tga", - {1.0f, -1.5f, 0.5f, 0.0f}, 1.0f, Constants::inf(), + {1.0f, -1.5f, 0.5f, 0.0f}, Color3{1.0f}, Color3{1.0f}, + 1.0f, Constants::inf(), {Containers::InPlaceInit, { /* Ambient isn't affected by light direction, otherwise it's a dot product of a normalized direction */ @@ -360,18 +362,23 @@ const struct { /* These two should produce the same output as the *normalized* dot product is the same */ {"directional, from the other side", "light-directional.tga", - {-1.0f, 1.5f, 0.5f, 0.0f}, 1.0f, Constants::inf(), {}}, + {-1.0f, 1.5f, 0.5f, 0.0f}, Color3{1.0f}, Color3{1.0f}, + 1.0f, Constants::inf(), {}}, {"directional, scaled direction", "light-directional.tga", - {10.0f, -15.0f, 5.0f, 0.0f}, 1.0f, Constants::inf(), {}}, + {10.0f, -15.0f, 5.0f, 0.0f}, Color3{1.0f}, Color3{1.0f}, + 1.0f, Constants::inf(), {}}, /* Range should have no effect either, especially zero range should not cause any NaNs */ {"directional, range=0.1", "light-directional.tga", - {1.0f, -1.5f, 0.5f, 0.0f}, 1.0f, 1.0f, {}}, + {1.0f, -1.5f, 0.5f, 0.0f}, Color3{1.0f}, Color3{1.0f}, + 1.0f, 1.0f, {}}, {"directional, range=0", "light-directional.tga", - {1.0f, -1.5f, 0.5f, 0.0f}, 1.0f, 1.0f, {}}, + {1.0f, -1.5f, 0.5f, 0.0f}, Color3{1.0f}, Color3{1.0f}, + 1.0f, 1.0f, {}}, /* Light from the other side doesn't contribute anything */ {"directional, from back", "light-none.tga", - {-1.0f, 1.5f, -0.5f, 0.0f}, 1.0f, Constants::inf(), + {-1.0f, 1.5f, -0.5f, 0.0f}, Color3{1.0f}, Color3{1.0f}, + 1.0f, Constants::inf(), {Containers::InPlaceInit, { /* Only ambient color left */ {{40, 40}, 0x222222_rgb} @@ -379,12 +386,14 @@ const struct { /* This is the same as above, except that twice the intensity causes it to be 2x brighter */ {"directional, intensity=2", "light-directional-intensity2.tga", - {1.0f, -1.5f, 0.5f, 0.0f}, 2.0f, 1.0f, + {1.0f, -1.5f, 0.5f, 0.0f}, Color3{1.0f}, Color3{1.0f}, + 2.0f, 1.0f, {Containers::InPlaceInit, { {{40, 40}, 0x222222_rgb + 0xff8080_rgb*dot(Vector3{1.0f, -1.5f, 0.5f}.normalized(), Vector3::zAxis())*2.0f} }}}, {"point", "light-point.tga", - {0.75f, -0.75f, -1.25f, 1.0f}, 1.0f, Constants::inf(), + {0.75f, -0.75f, -1.25f, 1.0f}, Color3{1.0f}, Color3{1.0f}, + 1.0f, Constants::inf(), {Containers::InPlaceInit, { /* The range is inf, so it doesn't get fully ambient even at the edge */ @@ -394,15 +403,31 @@ const struct { /* Specular highlight */ {{60, 19}, 0xfefefe_rgb} }}}, + {"point, specular material color", "light-point-specular-color.tga", + {0.75f, -0.75f, -1.25f, 1.0f}, 0x80ff80_rgbf, Color3{1.0f}, + 1.0f, Constants::inf(), + {Containers::InPlaceInit, { + /* Colored specular highlight */ + {{60, 19}, 0xf2fcb0_rgb} + }}}, + {"point, specular light color", "light-point-specular-color.tga", + {0.75f, -0.75f, -1.25f, 1.0f}, Color3{1.0f}, 0x80ff80_rgbf, + 1.0f, Constants::inf(), + {Containers::InPlaceInit, { + /* Colored specular highlight */ + {{60, 19}, 0xf2fcb0_rgb} + }}}, {"point, attenuated specular", "light-point-attenuated-specular.tga", - {1.0f, -1.0f, -0.25f, 1.0f}, 1.0f, 2.5f, + {1.0f, -1.0f, -0.25f, 1.0f}, Color3{1.0f}, Color3{1.0f}, + 1.0f, 2.5f, {Containers::InPlaceInit, { /* Specular highlight shouldn't be brighter than the attenuated intensity */ {{57, 22}, 0x665656_rgb} }}}, - {"point, range=1.5", "light-point-range1.5.tga", - {0.75f, -0.75f, -1.25f, 1.0f}, 1.0f, 1.5f, + {"point, range=1.5, specular color", "light-point-range1.5.tga", + {0.75f, -0.75f, -1.25f, 1.0f}, Color3{1.0f}, 0x80ff80_rgbf, + 1.0f, 1.5f, {Containers::InPlaceInit, { /* Color goes back to ambient at distance = 1.5 */ {{59, 60}, 0x222222_rgb}, @@ -410,21 +435,25 @@ const struct { {{19, 14}, 0x222222_rgb}, /* But the center and specular stays ~ the same */ {{63, 16}, 0xc57474_rgb}, - {{60, 19}, 0xfefcfc_rgb} + {{60, 19}, 0xf2fcb0_rgb} }}}, {"point, intensity=10, range=0.5", "light-point-intensity10-range0.5.tga", - {0.75f, -0.75f, -1.25f, 1.0f}, 10.0f, 0.5f, {}}, + {0.75f, -0.75f, -1.25f, 1.0f}, Color3{1.0f}, Color3{1.0f}, + 10.0f, 0.5f, {}}, /* Range ends right at the surface, so no contribution */ {"point, range=0.25", "light-none.tga", - {0.75f, -0.75f, -1.25f, 1.0f}, 1.0f, 0.25f, {}}, + {0.75f, -0.75f, -1.25f, 1.0f}, Color3{1.0f}, Color3{1.0f}, + 1.0f, 0.25f, {}}, /* Zero range should not cause any NaNs, so the ambient contribution is still there */ {"point, range=0.0", "light-none.tga", - {0.75f, -0.75f, -1.25f, 1.0f}, 1.0f, 0.0f, {}}, + {0.75f, -0.75f, -1.25f, 1.0f}, Color3{1.0f}, Color3{1.0f}, + 1.0f, 0.0f, {}}, /* Distance is 0, which means the direction is always prependicular and thus contributes nothing */ {"point, distance=0", "light-none.tga", - {0.75f, -0.75f, -1.25f, 1.0f}, 1.0f, 0.0f, {}} + {0.75f, -0.75f, -1.25f, 1.0f}, Color3{1.0f}, Color3{1.0f}, + 1.0f, 0.0f, {}} }; constexpr struct { @@ -1476,8 +1505,10 @@ void PhongGLTest::renderLights() { /* Set non-black ambient to catch accidental NaNs -- the render should never be fully black */ .setAmbientColor(0x222222_rgbf) + .setSpecularColor(data.specularColor) .setLightPositions({data.position}) .setLightColors({0xff8080_rgbf*data.intensity}) + .setLightSpecularColors({data.lightSpecularColor}) .setLightRanges({data.range}) .setShininess(60.0f) .setTransformationMatrix(transformation) @@ -1531,6 +1562,8 @@ void PhongGLTest::renderLightsSetOneByOne() { .setLightPosition(1, {0.75f, -0.75f, -1.25f, 1.0f}) .setLightColor(0, 0x00ffff_rgbf) .setLightColor(1, 0xff8080_rgbf) + .setLightSpecularColor(0, 0x0000ff_rgbf) + .setLightSpecularColor(1, 0x80ff80_rgbf) .setLightRange(0, Constants::inf()) .setLightRange(1, 1.5f) .setShininess(60.0f) diff --git a/src/Magnum/Shaders/Test/PhongTestFiles/light-point-range1.5.tga b/src/Magnum/Shaders/Test/PhongTestFiles/light-point-range1.5.tga index 4a0ff7aa29ed97d904dd635056f066a0279afd84..3ab90d057e9b4c30813651851eee24db3df787f5 100644 GIT binary patch delta 186 zcmbPbI?Hs!Z&uE<^i?@ITM7#HOlD!5$DfouKf7pMY5ktk+5<%;2PSW1n<^0!+7=x% zF}+}M!`uVYkKOKGbr~oL(h$c!Ng_C;IXb>SuW|X*Q^)Y6coOnS*1JSYS|n zWNdeK?ZS@D2Z8cH{XoOhCU4-FDqIy3-W-=P5hw@L3p5*O@#F~3>HGnKH9#4lE}*GE YTPOeHoX=GSGzVx8&>fqXbA9Fq03sGx?*IS* delta 186 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