From 903e2c213caa626b7636e92b2f1807cd401559ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sat, 5 Sep 2020 23:55:29 +0200 Subject: [PATCH] Shaders: properly use bitangents in Phong normal map calculation. --- doc/changelog.dox | 10 +++ src/Magnum/Shaders/Phong.cpp | 9 +- src/Magnum/Shaders/Phong.frag | 16 +++- src/Magnum/Shaders/Phong.h | 71 +++++++++++++++- src/Magnum/Shaders/Phong.vert | 33 +++++++- src/Magnum/Shaders/Test/CMakeLists.txt | 1 + src/Magnum/Shaders/Test/PhongGLTest.cpp | 78 +++++++++++++++--- .../PhongTestFiles/textured-normal-left.tga | Bin 0 -> 11105 bytes 8 files changed, 201 insertions(+), 17 deletions(-) create mode 100644 src/Magnum/Shaders/Test/PhongTestFiles/textured-normal-left.tga diff --git a/doc/changelog.dox b/doc/changelog.dox index 7c160fc7f..2534d0077 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -114,6 +114,16 @@ See also: - @ref magnum-sceneconverter "magnum-sceneconverter" now lists also materials and textures in `--info` +@subsubsection changelog-latest-changes-shaders Shaders library + +- In the original implementation of normal mapping in @ref Shaders::Phong, + there shader didn't provide a way to supply bitangent direction, forcing + users to patch normal maps. This is now possible using newly added + @ref Shaders::Phong::Tangent4, @ref Shaders::Phong::Bitangent attributes + and a @ref Shaders::Phong::Flag::Bitangent flag, implementing support for + both four-component tangents (used by glTF, for example) and separate + tangent and bitangent direction (used by Assimp). + @subsubsection changelog-latest-changes-trade Trade library - Recognizing TIFF file header magic in @ref Trade::AnyImageImporter "AnyImageImporter" diff --git a/src/Magnum/Shaders/Phong.cpp b/src/Magnum/Shaders/Phong.cpp index 18a10f990..bb8acc736 100644 --- a/src/Magnum/Shaders/Phong.cpp +++ b/src/Magnum/Shaders/Phong.cpp @@ -98,6 +98,7 @@ Phong::Phong(const Flags flags, const UnsignedInt lightCount): _flags{flags}, _l vert.addSource(flags & (Flag::AmbientTexture|Flag::DiffuseTexture|Flag::SpecularTexture|Flag::NormalTexture) ? "#define TEXTURED\n" : "") .addSource(flags & Flag::NormalTexture ? "#define NORMAL_TEXTURE\n" : "") + .addSource(flags & Flag::Bitangent ? "#define BITANGENT\n" : "") .addSource(flags & Flag::VertexColor ? "#define VERTEX_COLOR\n" : "") .addSource(flags & Flag::TextureTransformation ? "#define TEXTURE_TRANSFORMATION\n" : "") .addSource(Utility::formatString("#define LIGHT_COUNT {}\n", lightCount)) @@ -112,6 +113,7 @@ Phong::Phong(const Flags flags, const UnsignedInt lightCount): _flags{flags}, _l .addSource(flags & Flag::DiffuseTexture ? "#define DIFFUSE_TEXTURE\n" : "") .addSource(flags & Flag::SpecularTexture ? "#define SPECULAR_TEXTURE\n" : "") .addSource(flags & Flag::NormalTexture ? "#define NORMAL_TEXTURE\n" : "") + .addSource(flags & Flag::Bitangent ? "#define BITANGENT\n" : "") .addSource(flags & Flag::VertexColor ? "#define VERTEX_COLOR\n" : "") .addSource(flags & Flag::AlphaMask ? "#define ALPHA_MASK\n" : "") #ifndef MAGNUM_TARGET_GLES2 @@ -141,8 +143,11 @@ Phong::Phong(const Flags flags, const UnsignedInt lightCount): _flags{flags}, _l bindAttributeLocation(Position::Location, "position"); if(lightCount) bindAttributeLocation(Normal::Location, "normal"); - if((flags & Flag::NormalTexture) && lightCount) + if((flags & Flag::NormalTexture) && lightCount) { bindAttributeLocation(Tangent::Location, "tangent"); + if(flags & Flag::Bitangent) + bindAttributeLocation(Bitangent::Location, "bitangent"); + } if(flags & Flag::VertexColor) bindAttributeLocation(Color3::Location, "vertexColor"); /* Color4 is the same */ if(flags & (Flag::AmbientTexture|Flag::DiffuseTexture|Flag::SpecularTexture)) @@ -374,6 +379,7 @@ Debug& operator<<(Debug& debug, const Phong::Flag value) { _c(DiffuseTexture) _c(SpecularTexture) _c(NormalTexture) + _c(Bitangent) _c(AlphaMask) _c(VertexColor) _c(TextureTransformation) @@ -396,6 +402,7 @@ Debug& operator<<(Debug& debug, const Phong::Flags value) { Phong::Flag::DiffuseTexture, Phong::Flag::SpecularTexture, Phong::Flag::NormalTexture, + Phong::Flag::Bitangent, Phong::Flag::AlphaMask, Phong::Flag::VertexColor, Phong::Flag::InstancedTextureOffset, /* Superset of TextureTransformation */ diff --git a/src/Magnum/Shaders/Phong.frag b/src/Magnum/Shaders/Phong.frag index 05e284e69..a6edc6849 100644 --- a/src/Magnum/Shaders/Phong.frag +++ b/src/Magnum/Shaders/Phong.frag @@ -154,7 +154,12 @@ uniform lowp vec4 lightColors[LIGHT_COUNT] #if LIGHT_COUNT in mediump vec3 transformedNormal; #ifdef NORMAL_TEXTURE +#ifndef BITANGENT +in mediump vec4 transformedTangent; +#else in mediump vec3 transformedTangent; +in mediump vec3 transformedBitangent; +#endif #endif in highp vec3 lightDirections[LIGHT_COUNT]; in highp vec3 cameraDirection; @@ -218,11 +223,20 @@ void main() { /* Normal */ mediump vec3 normalizedTransformedNormal = normalize(transformedNormal); #ifdef NORMAL_TEXTURE + #ifndef BITANGENT + mediump vec3 normalizedTransformedTangent = normalize(transformedTangent.xyz); + #else mediump vec3 normalizedTransformedTangent = normalize(transformedTangent); + mediump vec3 normalizedTransformedBitangent = normalize(transformedBitangent); + #endif mediump mat3 tbn = mat3( normalizedTransformedTangent, + #ifndef BITANGENT normalize(cross(normalizedTransformedNormal, - normalizedTransformedTangent)), + normalizedTransformedTangent)*transformedTangent.w), + #else + normalizedTransformedBitangent, + #endif normalizedTransformedNormal ); normalizedTransformedNormal = tbn*(normalize((texture(normalTexture, interpolatedTextureCoordinates).rgb*2.0 - vec3(1.0))*vec3(normalTextureScale, normalTextureScale, 1.0))); diff --git a/src/Magnum/Shaders/Phong.h b/src/Magnum/Shaders/Phong.h index 6e03e6143..1e48b73c0 100644 --- a/src/Magnum/Shaders/Phong.h +++ b/src/Magnum/Shaders/Phong.h @@ -100,6 +100,35 @@ diffuse part and then separate the alpha like this: @snippet MagnumShaders.cpp Phong-usage-alpha +@section Shaders-Phong-normal-mapping Normal mapping + +If you want to use normal textures, enable @ref Flag::NormalTexture and call +@ref bindNormalTexture(). In addition you need to supply per-vertex tangent and +bitangent direction: + +- either using a four-component @ref Tangent4 attribute, where the sign of + the fourth component defines handedness of tangent basis, as described in + @ref Trade::MeshAttribute::Tangent; +- or a using pair of three-component @ref Tangent and @ref Bitangent + attributes together with enabling @ref Flag::Bitangent + +If you supply just a three-component @ref Tangent attribute and no bitangents, +the shader will implicitly assume the fourth component to be @cpp 1.0f @ce, +forming a right-handed tangent space. This is a valid optimization when you +have full control over the bitangent orientation, but won't work with general +meshes. + +@m_class{m-note m-success} + +@par + You can also use the @ref MeshVisualizer3D shader to visualize and debug + per-vertex normal, tangent and binormal direction, among other things. + +The strength of the effect can be controlled by +@ref setNormalTextureScale(). See +@ref Trade::MaterialAttribute::NormalTextureScale for a description of the +factor is used. + @section Shaders-Phong-object-id Object ID output The shader supports writing object ID to the framebuffer for object picking or @@ -177,11 +206,40 @@ class MAGNUM_SHADERS_EXPORT Phong: public GL::AbstractShaderProgram { * @m_since{2019,10} * * @ref shaders-generic "Generic attribute", - * @ref Magnum::Vector3 "Vector3", used only if - * @ref Flag::NormalTexture is set. + * @ref Magnum::Vector3 "Vector3". Use either this or the @ref Tangent4 + * attribute. If only a three-component attribute is used and + * @ref Flag::Bitangent is not enabled, it's the same as if + * @ref Tangent4 was specified with the fourth component always being + * @cpp 1.0f @ce. Used only if @ref Flag::NormalTexture is set. + * @see @ref Shaders-Phong-normal-mapping */ typedef Generic3D::Tangent Tangent; + /** + * @brief Tangent direction with a bitangent sign + * @m_since_latest + * + * @ref shaders-generic "Generic attribute", + * @ref Magnum::Vector4 "Vector4". Use either this or the @ref Tangent + * attribute. If @ref Flag::Bitangent is set, the fourth component is + * ignored and bitangents are taken from the @ref Bitangent attribute + * instead. Used only if @ref Flag::NormalTexture is set. + * @see @ref Shaders-Phong-normal-mapping + */ + typedef typename Generic3D::Tangent4 Tangent4; + + /** + * @brief Bitangent direction + * @m_since_latest + * + * @ref shaders-generic "Generic attribute", + * @ref Magnum::Vector3 "Vector3". Use either this or the @ref Tangent4 + * attribute. Used only if both @ref Flag::NormalTexture and + * @ref Flag::Bitangent are set. + * @see @ref Shaders-Phong-normal-mapping + */ + typedef typename Generic3D::Bitangent Bitangent; + /** * @brief 2D texture coordinates * @@ -348,6 +406,15 @@ class MAGNUM_SHADERS_EXPORT Phong: public GL::AbstractShaderProgram { */ VertexColor = 1 << 5, + /** + * Use the separate @ref Bitangent attribute for retrieving vertex + * bitangents. If this flag is not present, the last component of + * @ref Tangent4 is used to calculate bitangent direction. See + * @ref Shaders-Phong-normal-mapping for more information. + * @m_since_latest + */ + Bitangent = 1 << 11, + /** * Enable texture coordinate transformation. If this flag is set, * the shader expects that at least one of diff --git a/src/Magnum/Shaders/Phong.vert b/src/Magnum/Shaders/Phong.vert index 981063770..0eb740916 100644 --- a/src/Magnum/Shaders/Phong.vert +++ b/src/Magnum/Shaders/Phong.vert @@ -95,7 +95,20 @@ in mediump vec3 normal; #ifdef EXPLICIT_ATTRIB_LOCATION layout(location = TANGENT_ATTRIBUTE_LOCATION) #endif -in mediump vec3 tangent; +in mediump + #ifndef BITANGENT + vec4 + #else + vec3 + #endif + tangent; +#endif + +#ifdef BITANGENT +#ifdef EXPLICIT_ATTRIB_LOCATION +layout(location = BITANGENT_ATTRIBUTE_LOCATION) +#endif +in mediump vec3 bitangent; #endif #endif @@ -148,7 +161,12 @@ in mediump vec2 instancedTextureOffset; #if LIGHT_COUNT out mediump vec3 transformedNormal; #ifdef NORMAL_TEXTURE +#ifndef BITANGENT +out mediump vec4 transformedTangent; +#else out mediump vec3 transformedTangent; +out mediump vec3 transformedBitangent; +#endif #endif out highp vec3 lightDirections[LIGHT_COUNT]; out highp vec3 cameraDirection; @@ -171,11 +189,24 @@ void main() { #endif normal; #ifdef NORMAL_TEXTURE + #ifndef BITANGENT + transformedTangent = vec4(normalMatrix* + #ifdef INSTANCED_TRANSFORMATION + instancedNormalMatrix* + #endif + tangent.xyz, tangent.w); + #else transformedTangent = normalMatrix* #ifdef INSTANCED_TRANSFORMATION instancedNormalMatrix* #endif tangent; + transformedBitangent = normalMatrix* + #ifdef INSTANCED_TRANSFORMATION + instancedNormalMatrix* + #endif + bitangent; + #endif #endif /* Direction to the light */ diff --git a/src/Magnum/Shaders/Test/CMakeLists.txt b/src/Magnum/Shaders/Test/CMakeLists.txt index 83cd3f7e7..9781971f1 100644 --- a/src/Magnum/Shaders/Test/CMakeLists.txt +++ b/src/Magnum/Shaders/Test/CMakeLists.txt @@ -234,6 +234,7 @@ if(BUILD_GL_TESTS) PhongTestFiles/textured-diffuse.tga PhongTestFiles/textured-diffuse-transformed.tga PhongTestFiles/textured-normal.tga + PhongTestFiles/textured-normal-left.tga PhongTestFiles/textured-normal0.0.tga PhongTestFiles/textured-normal0.5.tga PhongTestFiles/textured-specular.tga diff --git a/src/Magnum/Shaders/Test/PhongGLTest.cpp b/src/Magnum/Shaders/Test/PhongGLTest.cpp index 4e8e01ea6..76217abfb 100644 --- a/src/Magnum/Shaders/Test/PhongGLTest.cpp +++ b/src/Magnum/Shaders/Test/PhongGLTest.cpp @@ -145,6 +145,8 @@ constexpr struct { {"diffuse texture + texture transform", Phong::Flag::DiffuseTexture|Phong::Flag::TextureTransformation, 1}, {"specular texture", Phong::Flag::SpecularTexture, 1}, {"normal texture", Phong::Flag::NormalTexture, 1}, + {"normal texture + separate bitangents", Phong::Flag::NormalTexture|Phong::Flag::Bitangent, 1}, + {"separate bitangents alone", Phong::Flag::Bitangent, 1}, {"ambient + diffuse texture", Phong::Flag::AmbientTexture|Phong::Flag::DiffuseTexture, 1}, {"ambient + specular texture", Phong::Flag::AmbientTexture|Phong::Flag::SpecularTexture, 1}, {"diffuse + specular texture", Phong::Flag::DiffuseTexture|Phong::Flag::SpecularTexture, 1}, @@ -210,13 +212,52 @@ const struct { bool multiBind; Deg rotation; Float scale; + Vector4 tangent; + Vector3 bitangent; + Shaders::Phong::Tangent4::Components tangentComponents; + bool flipNormalY; + Shaders::Phong::Flags flags; } RenderTexturedNormalData[]{ - {"", "textured-normal.tga", false, {}, 1.0f}, - {"multi bind", "textured-normal.tga", true, {}, 1.0f}, - {"rotated 90°", "textured-normal.tga", false, 90.0_degf, 1.0f}, - {"rotated -90°", "textured-normal.tga", false, -90.0_degf, 1.0f}, - {"0.5 scale", "textured-normal0.5.tga", false, {}, 0.5f}, - {"0.0 scale", "textured-normal0.0.tga", false, {}, 0.0f} + {"", "textured-normal.tga", false, {}, 1.0f, + {1.0f, 0.0f, 0.0f, 1.0f}, {}, + Shaders::Phong::Tangent4::Components::Four, false, {}}, + {"multi bind", "textured-normal.tga", true, {}, 1.0f, + {1.0f, 0.0f, 0.0f, 1.0f}, {}, + Shaders::Phong::Tangent4::Components::Four, false, {}}, + {"rotated 90°", "textured-normal.tga", false, 90.0_degf, 1.0f, + {1.0f, 0.0f, 0.0f, 1.0f}, {}, + Shaders::Phong::Tangent4::Components::Four, false, {}}, + {"rotated -90°", "textured-normal.tga", false, -90.0_degf, 1.0f, + {1.0f, 0.0f, 0.0f, 1.0f}, {}, + Shaders::Phong::Tangent4::Components::Four, false, {}}, + {"0.5 scale", "textured-normal0.5.tga", false, {}, 0.5f, + {1.0f, 0.0f, 0.0f, 1.0f}, {}, + Shaders::Phong::Tangent4::Components::Four, false, {}}, + {"0.0 scale", "textured-normal0.0.tga", false, {}, 0.0f, + {1.0f, 0.0f, 0.0f, 1.0f}, {}, + Shaders::Phong::Tangent4::Components::Four, false, {}}, + /* The fourth component, if missing, gets automatically filled up to 1, + so this should work */ + {"implicit bitangent direction", "textured-normal.tga", false, {}, 1.0f, + {1.0f, 0.0f, 0.0f, 0.0f}, {}, + Shaders::Phong::Tangent4::Components::Three, false, {}}, + {"separate bitangents", "textured-normal.tga", false, {}, 1.0f, + {1.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 1.0f, 0.0f}, + Shaders::Phong::Tangent4::Components::Three, false, + Shaders::Phong::Flag::Bitangent}, + {"right-handed, flipped Y", "textured-normal-left.tga", false, {}, 1.0f, + {1.0f, 0.0f, 0.0f, 1.0f}, {}, + Shaders::Phong::Tangent4::Components::Four, true, {}}, + {"left-handed", "textured-normal-left.tga", false, {}, 1.0f, + {1.0f, 0.0f, 0.0f, -1.0f}, {}, + Shaders::Phong::Tangent4::Components::Four, false, {}}, + {"left-handed, separate bitangents", "textured-normal-left.tga", false, {}, 1.0f, + {1.0f, 0.0f, 0.0f, 0.0f}, {0.0f, -1.0f, 0.0f}, + Shaders::Phong::Tangent4::Components::Three, false, + Shaders::Phong::Flag::Bitangent}, + {"left-handed, flipped Y", "textured-normal.tga", false, {}, 1.0f, + {1.0f, 0.0f, 0.0f, -1.0f}, {}, + Shaders::Phong::Tangent4::Components::Four, true, {}} }; const struct { @@ -903,9 +944,14 @@ void PhongGLTest::renderTexturedNormal() { Containers::Pointer importer = _manager.loadAndInstantiate("AnyImageImporter"); CORRADE_VERIFY(importer); - GL::Texture2D normal; + /* Normal texture. Flip normal Y, if requested */ Containers::Optional image; CORRADE_VERIFY(importer->openFile(Utility::Directory::join(_testDir, "TestFiles/normal-texture.tga")) && (image = importer->image2D(0))); + if(data.flipNormalY) for(auto row: image->mutablePixels()) + for(Color3ub& pixel: row) + pixel.y() = 255 - pixel.y(); + + GL::Texture2D normal; normal.setMinificationFilter(GL::SamplerFilter::Linear) .setMagnificationFilter(GL::SamplerFilter::Linear) .setWrapping(GL::SamplerWrapping::ClampToEdge) @@ -914,16 +960,24 @@ void PhongGLTest::renderTexturedNormal() { GL::Mesh plane = MeshTools::compile(Primitives::planeSolid( Primitives::PlaneFlag::TextureCoordinates)); - /* Add hardcoded tangents */ - /** @todo remove once MeshData is sane */ + /* Add tangents / bitangents of desired component count. Unused components + are set to zero to ensure the shader doesn't use them. */ + const struct TangentBitangent { + Vector4 tangent; + Vector3 bitangent; + } tangentBitangent{data.tangent, data.bitangent}; GL::Buffer tangents; - tangents.setData(Containers::Array{Containers::DirectInit, 4, Vector3::xAxis()}); - plane.addVertexBuffer(std::move(tangents), 0, Shaders::Phong::Tangent{}); + tangents.setData(Containers::Array{Containers::DirectInit, 4, tangentBitangent}); + plane.addVertexBuffer(tangents, 0, sizeof(TangentBitangent), + GL::DynamicAttribute{Shaders::Phong::Tangent4{data.tangentComponents}}); + plane.addVertexBuffer(std::move(tangents), sizeof(Vector4), + sizeof(TangentBitangent), + GL::DynamicAttribute{Shaders::Phong::Bitangent{}}); /* Rotating the view a few times (together with light positions). If the tangent transformation in the shader is correct, it should result in exactly the same images. */ - Phong shader{Phong::Flag::NormalTexture, 2}; + Phong shader{Phong::Flag::NormalTexture|data.flags, 2}; shader.setLightPositions({ Matrix4::rotationZ(data.rotation).transformPoint({-3.0f, -3.0f, 0.0f}), Matrix4::rotationZ(data.rotation).transformPoint({ 3.0f, -3.0f, 0.0f})}) diff --git a/src/Magnum/Shaders/Test/PhongTestFiles/textured-normal-left.tga b/src/Magnum/Shaders/Test/PhongTestFiles/textured-normal-left.tga new file mode 100644 index 0000000000000000000000000000000000000000..7bf697376894d53f321c8807a097917ac1fe1e3c GIT binary patch literal 11105 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 Jzg{Q*{TCMd6CMBn literal 0 HcmV?d00001