From 55685ea382ac65171fcc2c7826c7cba9443708bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Mon, 6 Apr 2020 00:56:50 +0200 Subject: [PATCH] Shaders: implement instancing in Phong. --- doc/changelog.dox | 2 +- doc/snippets/MagnumShaders.cpp | 22 +++ src/Magnum/Shaders/Phong.cpp | 13 +- src/Magnum/Shaders/Phong.h | 113 ++++++++++++- src/Magnum/Shaders/Phong.vert | 43 ++++- src/Magnum/Shaders/Test/CMakeLists.txt | 2 + src/Magnum/Shaders/Test/PhongGLTest.cpp | 160 +++++++++++++++++- src/Magnum/Shaders/Test/PhongTest.cpp | 32 ++-- .../Test/PhongTestFiles/instanced-normal.tga | Bin 0 -> 9469 bytes .../Shaders/Test/PhongTestFiles/instanced.tga | Bin 0 -> 8962 bytes 10 files changed, 356 insertions(+), 31 deletions(-) create mode 100644 src/Magnum/Shaders/Test/PhongTestFiles/instanced-normal.tga create mode 100644 src/Magnum/Shaders/Test/PhongTestFiles/instanced.tga diff --git a/doc/changelog.dox b/doc/changelog.dox index 151f27d93..60743d8bb 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -233,7 +233,7 @@ See also: @ref Shaders::Generic::TransformationMatrix, @ref Shaders::Generic::NormalMatrix and @ref Shaders::Generic::TextureOffset for instancing -- Instancing in @ref Shaders::Flat +- Instancing in @ref Shaders::Flat and @ref Shaders::Phong @subsubsection changelog-latest-new-trade Trade library diff --git a/doc/snippets/MagnumShaders.cpp b/doc/snippets/MagnumShaders.cpp index a871a4094..55b3e5a9f 100644 --- a/doc/snippets/MagnumShaders.cpp +++ b/doc/snippets/MagnumShaders.cpp @@ -269,6 +269,28 @@ mesh.setInstanceCount(Containers::arraySize(instanceData)) /* [Flat-usage-instancing] */ } +{ +GL::Mesh mesh; +/* [Phong-usage-instancing] */ +struct { + Matrix4 transformation; + Matrix3x3 normal; +} instanceData[] { + {Matrix4::translation({1.0f, 2.0f, 0.0f})*Matrix4::rotationX(90.0_degf), {}}, + {Matrix4::translation({2.0f, 1.0f, 0.0f})*Matrix4::rotationY(90.0_degf), {}}, + {Matrix4::translation({3.0f, 0.0f, 1.0f})*Matrix4::rotationZ(90.0_degf), {}}, + // ... +}; +for(auto& instance: instanceData) + instance.normal = instance.transformation.normalMatrix(); + +mesh.setInstanceCount(Containers::arraySize(instanceData)) + .addVertexBufferInstanced(GL::Buffer{instanceData}, 1, 0, + Shaders::Phong::TransformationMatrix{}, + Shaders::Phong::NormalMatrix{}); +/* [Phong-usage-instancing] */ +} + { /* [MeshVisualizer-usage-geom1] */ struct Vertex { diff --git a/src/Magnum/Shaders/Phong.cpp b/src/Magnum/Shaders/Phong.cpp index 5caeff18b..72238fb2c 100644 --- a/src/Magnum/Shaders/Phong.cpp +++ b/src/Magnum/Shaders/Phong.cpp @@ -104,6 +104,8 @@ Phong::Phong(const Flags flags, const UnsignedInt lightCount): _flags{flags}, _l #ifndef MAGNUM_TARGET_GLES2 .addSource(flags >= Flag::InstancedObjectId ? "#define INSTANCED_OBJECT_ID\n" : "") #endif + .addSource(flags & Flag::InstancedTransformation ? "#define INSTANCED_TRANSFORMATION\n" : "") + .addSource(flags >= Flag::InstancedTextureOffset ? "#define INSTANCED_TEXTURE_OFFSET\n" : "") .addSource(rs.get("generic.glsl")) .addSource(rs.get("Phong.vert")); frag.addSource(flags & Flag::AmbientTexture ? "#define AMBIENT_TEXTURE\n" : "") @@ -152,6 +154,10 @@ Phong::Phong(const Flags flags, const UnsignedInt lightCount): _flags{flags}, _l } if(flags >= Flag::InstancedObjectId) bindAttributeLocation(ObjectId::Location, "instanceObjectId"); + if(flags & Flag::InstancedTransformation) + bindAttributeLocation(TransformationMatrix::Location, "instancedTransformationMatrix"); + if(flags >= Flag::InstancedTextureOffset) + bindAttributeLocation(TextureOffset::Location, "instancedTextureOffset"); #endif } #endif @@ -364,6 +370,8 @@ Debug& operator<<(Debug& debug, const Phong::Flag value) { _c(InstancedObjectId) _c(ObjectId) #endif + _c(InstancedTransformation) + _c(InstancedTextureOffset) #undef _c /* LCOV_EXCL_STOP */ } @@ -379,12 +387,13 @@ Debug& operator<<(Debug& debug, const Phong::Flags value) { Phong::Flag::NormalTexture, Phong::Flag::AlphaMask, Phong::Flag::VertexColor, + Phong::Flag::InstancedTextureOffset, /* Superset of TextureTransformation */ Phong::Flag::TextureTransformation, #ifndef MAGNUM_TARGET_GLES2 Phong::Flag::InstancedObjectId, /* Superset of ObjectId */ - Phong::Flag::ObjectId + Phong::Flag::ObjectId, #endif - }); + Phong::Flag::InstancedTransformation}); } }} diff --git a/src/Magnum/Shaders/Phong.h b/src/Magnum/Shaders/Phong.h index 33fc31df7..8b66448e4 100644 --- a/src/Magnum/Shaders/Phong.h +++ b/src/Magnum/Shaders/Phong.h @@ -106,6 +106,30 @@ example. @requires_gles30 Object ID output requires integer buffer attachments, which are not available in OpenGL ES 2.0 or WebGL 1.0. +@section Shaders-Phong-instancing Instanced rendering + +Enabling @ref Flag::InstancedTransformation will turn the shader into an +instanced one. It'll take per-instance transformation and normal matrix from +the @ref TransformationMatrix and @ref NormalMatrix attributes, applying those +before the matrix set by @ref setTransformationMatrix() and +@ref setNormalMatrix(). Besides that, @ref Flag::VertexColor (and the +@ref Color3 / @ref Color4) attributes can work as both per-vertex and +per-instance, and for texturing it's possible to have per-instance texture +offset taken from @ref TextureOffset when @ref Flag::InstancedTextureOffset is +enabled (similarly to transformation, applied before @ref setTextureMatrix()). +The snippet below shows adding a buffer with per-instance transformation to a +mesh --- note how a normal matrix attribute has to be populated and supplied as +well to ensure lighting works: + +@snippet MagnumShaders.cpp Phong-usage-instancing + +@requires_gl33 Extension @gl_extension{ARB,instanced_arrays} +@requires_gles30 Extension @gl_extension{ANGLE,instanced_arrays}, + @gl_extension{EXT,instanced_arrays} or @gl_extension{NV,instanced_arrays} + in OpenGL ES 2.0. +@requires_webgl20 Extension @webgl_extension{ANGLE,instanced_arrays} in WebGL + 1.0. + @section Shaders-Phong-zero-lights Zero lights Creating this shader with zero lights makes its output equivalent to the @@ -113,7 +137,10 @@ Creating this shader with zero lights makes its output equivalent to the (if @ref Flag::AmbientTexture is enabled) are taken into account, which correspond to @ref Flat::setColor() and @ref Flat::bindTexture(). This is useful to reduce complexity in apps that render models with pre-baked lights. -In addition, enabling @ref Flag::VertexColor and using a default ambient color with no texturing makes this shader equivalent to @ref VertexColor. +For instanced workflows using zero lights means the @ref NormalMatrix instance +attribute doesn't need to be supplied either. In addition, enabling +@ref Flag::VertexColor and using a default ambient color with no texturing +makes this shader equivalent to @ref VertexColor. @see @ref shaders */ @@ -189,6 +216,51 @@ class MAGNUM_SHADERS_EXPORT Phong: public GL::AbstractShaderProgram { typedef Generic3D::ObjectId ObjectId; #endif + /** + * @brief (Instanced) transformation matrix + * @m_since_latest + * + * @ref shaders-generic "Generic attribute", @ref Magnum::Matrix4. + * Used only if @ref Flag::InstancedTransformation is set. + * @requires_gl33 Extension @gl_extension{ARB,instanced_arrays} + * @requires_gles30 Extension @gl_extension{ANGLE,instanced_arrays}, + * @gl_extension{EXT,instanced_arrays} or + * @gl_extension{NV,instanced_arrays} in OpenGL ES 2.0. + * @requires_webgl20 Extension @webgl_extension{ANGLE,instanced_arrays} + * in WebGL 1.0. + */ + typedef Generic3D::TransformationMatrix TransformationMatrix; + + /** + * @brief (Instanced) normal matrix + * @m_since_latest + * + * @ref shaders-generic "Generic attribute", @ref Magnum::Matrix3x3. + * Used only if @ref Flag::InstancedTransformation is set. + * @requires_gl33 Extension @gl_extension{ARB,instanced_arrays} + * @requires_gles30 Extension @gl_extension{ANGLE,instanced_arrays}, + * @gl_extension{EXT,instanced_arrays} or + * @gl_extension{NV,instanced_arrays} in OpenGL ES 2.0. + * @requires_webgl20 Extension @webgl_extension{ANGLE,instanced_arrays} + * in WebGL 1.0. + */ + typedef Generic3D::NormalMatrix NormalMatrix; + + /** + * @brief (Instanced) texture offset + * @m_since_latest + * + * @ref shaders-generic "Generic attribute", @ref Magnum::Vector2. Used + * only if @ref Flag::InstancedTextureOffset is set. + * @requires_gl33 Extension @gl_extension{ARB,instanced_arrays} + * @requires_gles30 Extension @gl_extension{ANGLE,instanced_arrays}, + * @gl_extension{EXT,instanced_arrays} or + * @gl_extension{NV,instanced_arrays} in OpenGL ES 2.0. + * @requires_webgl20 Extension @webgl_extension{ANGLE,instanced_arrays} + * in WebGL 1.0. + */ + typedef typename Generic3D::TextureOffset TextureOffset; + enum: UnsignedInt { /** * Color shader output. @ref shaders-generic "Generic output", @@ -298,8 +370,45 @@ class MAGNUM_SHADERS_EXPORT Phong: public GL::AbstractShaderProgram { * WebGL 1.0. * @m_since_latest */ - InstancedObjectId = (1 << 8)|ObjectId + InstancedObjectId = (1 << 8)|ObjectId, #endif + + /** + * Instanced transformation. Retrieves a per-instance + * transformation and normal matrix from the + * @ref TransformationMatrix / @ref NormalMatrix attributes and + * uses them together with matrices coming from + * @ref setTransformationMatrix() and @ref setNormalMatrix() (first + * the per-instance, then the uniform matrix). See + * @ref Shaders-Phong-instancing for more information. + * @requires_gl33 Extension @gl_extension{ARB,instanced_arrays} + * @requires_gles30 Extension @gl_extension{ANGLE,instanced_arrays}, + * @gl_extension{EXT,instanced_arrays} or + * @gl_extension{NV,instanced_arrays} in OpenGL ES 2.0. + * @requires_webgl20 Extension @webgl_extension{ANGLE,instanced_arrays} + * in WebGL 1.0. + * @m_since_latest + */ + InstancedTransformation = 1 << 9, + + /** + * Instanced texture offset. Retrieves a per-instance offset vector + * from the @ref TextureOffset attribute and uses it together with + * the matrix coming from @ref setTextureMatrix() (first the + * per-instance vector, then the uniform matrix). Instanced texture + * scaling and rotation is not supported at the moment, you can + * specify that only via the uniform @ref setTextureMatrix(). + * Implicitly enables @ref Flag::TextureTransformation. See + * @ref Shaders-Phong-instancing for more information. + * @requires_gl33 Extension @gl_extension{ARB,instanced_arrays} + * @requires_gles30 Extension @gl_extension{ANGLE,instanced_arrays}, + * @gl_extension{EXT,instanced_arrays} or + * @gl_extension{NV,instanced_arrays} in OpenGL ES 2.0. + * @requires_webgl20 Extension @webgl_extension{ANGLE,instanced_arrays} + * in WebGL 1.0. + * @m_since_latest + */ + InstancedTextureOffset = (1 << 10)|TextureTransformation }; /** diff --git a/src/Magnum/Shaders/Phong.vert b/src/Magnum/Shaders/Phong.vert index 6e86a4513..2868f706d 100644 --- a/src/Magnum/Shaders/Phong.vert +++ b/src/Magnum/Shaders/Phong.vert @@ -122,6 +122,25 @@ in highp uint instanceObjectId; flat out highp uint interpolatedInstanceObjectId; #endif +#ifdef INSTANCED_TRANSFORMATION +#ifdef EXPLICIT_ATTRIB_LOCATION +layout(location = TRANSFORMATION_MATRIX_ATTRIBUTE_LOCATION) +#endif +in highp mat4 instancedTransformationMatrix; + +#ifdef EXPLICIT_ATTRIB_LOCATION +layout(location = NORMAL_MATRIX_ATTRIBUTE_LOCATION) +#endif +in highp mat3 instancedNormalMatrix; +#endif + +#ifdef INSTANCED_TEXTURE_OFFSET +#ifdef EXPLICIT_ATTRIB_LOCATION +layout(location = TEXTURE_OFFSET_ATTRIBUTE_LOCATION) +#endif +in mediump vec2 instancedTextureOffset; +#endif + #if LIGHT_COUNT out mediump vec3 transformedNormal; #ifdef NORMAL_TEXTURE @@ -133,14 +152,26 @@ out highp vec3 cameraDirection; void main() { /* Transformed vertex position */ - highp vec4 transformedPosition4 = transformationMatrix*position; + highp vec4 transformedPosition4 = transformationMatrix* + #ifdef INSTANCED_TRANSFORMATION + instancedTransformationMatrix* + #endif + position; highp vec3 transformedPosition = transformedPosition4.xyz/transformedPosition4.w; #if LIGHT_COUNT /* Transformed normal and tangent vector */ - transformedNormal = normalMatrix*normal; + transformedNormal = normalMatrix* + #ifdef INSTANCED_TRANSFORMATION + instancedNormalMatrix* + #endif + normal; #ifdef NORMAL_TEXTURE - transformedTangent = normalMatrix*tangent; + transformedTangent = normalMatrix* + #ifdef INSTANCED_TRANSFORMATION + instancedNormalMatrix* + #endif + tangent; #endif /* Direction to the light */ @@ -158,7 +189,11 @@ void main() { /* Texture coordinates, if needed */ interpolatedTextureCoordinates = #ifdef TEXTURE_TRANSFORMATION - (textureMatrix*vec3(textureCoordinates, 1.0)).xy + (textureMatrix*vec3( + #ifdef INSTANCED_TEXTURE_OFFSET + instancedTextureOffset + + #endif + textureCoordinates, 1.0)).xy #else textureCoordinates #endif diff --git a/src/Magnum/Shaders/Test/CMakeLists.txt b/src/Magnum/Shaders/Test/CMakeLists.txt index 688a6c862..a86a7726c 100644 --- a/src/Magnum/Shaders/Test/CMakeLists.txt +++ b/src/Magnum/Shaders/Test/CMakeLists.txt @@ -211,6 +211,8 @@ if(BUILD_GL_TESTS) PhongTestFiles/colored.tga PhongTestFiles/defaults.tga + PhongTestFiles/instanced.tga + PhongTestFiles/instanced-normal.tga PhongTestFiles/shininess-black-specular.tga PhongTestFiles/shininess0-overflow.tga PhongTestFiles/shininess0.tga diff --git a/src/Magnum/Shaders/Test/PhongGLTest.cpp b/src/Magnum/Shaders/Test/PhongGLTest.cpp index b17082d06..59b20b029 100644 --- a/src/Magnum/Shaders/Test/PhongGLTest.cpp +++ b/src/Magnum/Shaders/Test/PhongGLTest.cpp @@ -35,6 +35,7 @@ #include "Magnum/PixelFormat.h" #include "Magnum/DebugTools/CompareImage.h" #include "Magnum/GL/Context.h" +#include "Magnum/GL/Extensions.h" #include "Magnum/GL/Framebuffer.h" #include "Magnum/GL/Mesh.h" #include "Magnum/GL/OpenGLTester.h" @@ -104,6 +105,8 @@ struct PhongGLTest: GL::OpenGLTester { void renderZeroLights(); + void renderInstanced(); + private: PluginManager::Manager _manager{"nonexistent"}; std::string _testDir; @@ -121,12 +124,12 @@ struct PhongGLTest: GL::OpenGLTester { - Mesa Intel - Mesa AMD - SwiftShader ES2/ES3 - - ARM Mali (Huawei P10) ES2/ES3 - - WebGL 1 / 2 (on Mesa Intel) - - NVidia Windows - - Intel Windows - - AMD on macOS - - iPhone 6 w/ iOS 12.4 + - ARM Mali (Huawei P10) ES2/ES3 (except instancing) + - WebGL 1 / 2 (on Mesa Intel) (except instancing) + - NVidia Windows (except instancing) + - Intel Windows (except instancing) + - AMD on macOS (except instancing) + - iPhone 6 w/ iOS 12.4 (except instancing) */ constexpr struct { @@ -155,7 +158,10 @@ constexpr struct { {"object ID + alpha mask + specular texture", Phong::Flag::ObjectId|Phong::Flag::AlphaMask|Phong::Flag::SpecularTexture, 1}, #endif {"five lights", {}, 5}, - {"zero lights", {}, 0} + {"zero lights", {}, 0}, + {"instanced transformation", Phong::Flag::InstancedTransformation, 3}, + {"instanced specular texture offset", Phong::Flag::SpecularTexture|Phong::Flag::InstancedTextureOffset, 3}, + {"instanced normal texture offset", Phong::Flag::NormalTexture|Phong::Flag::InstancedTextureOffset, 3} }; using namespace Math::Literals; @@ -284,6 +290,32 @@ constexpr struct { }; #endif +constexpr struct { + const char* name; + const char* file; + Phong::Flags flags; + Float maxThreshold, meanThreshold; +} RenderInstancedData[] { + {"diffuse", "instanced.tga", {}, + #if !(defined(MAGNUM_TARGET_GLES2) && defined(MAGNUM_TARGET_WEBGL)) + /* AMD has one off pixel; SwiftShader a bit more */ + 93.67f, 0.106f, + #else + /* WebGL 1 doesn't have 8bit renderbuffer storage */ + 93.67f, 0.106f, + #endif + }, + {"diffuse + normal", "instanced-normal.tga", Phong::Flag::NormalTexture, + #if !(defined(MAGNUM_TARGET_GLES2) && defined(MAGNUM_TARGET_WEBGL)) + /* AMD has one off pixel */ + 94.0f, 0.132f, + #else + /* WebGL 1 doesn't have 8bit renderbuffer storage */ + 94.0f, 0.132f, + #endif + } +}; + PhongGLTest::PhongGLTest() { addInstancedTests({&PhongGLTest::construct}, Containers::arraySize(ConstructData)); @@ -356,6 +388,11 @@ PhongGLTest::PhongGLTest() { #endif ); + addInstancedTests({&PhongGLTest::renderInstanced}, + Containers::arraySize(RenderInstancedData), + &PhongGLTest::renderSetup, + &PhongGLTest::renderTeardown); + /* Load the plugins directly from the build tree. Otherwise they're either static and already loaded or not present in the build tree */ #ifdef ANYIMAGEIMPORTER_PLUGIN_FILENAME @@ -1332,6 +1369,115 @@ void PhongGLTest::renderZeroLights() { #endif } +void PhongGLTest::renderInstanced() { + auto&& data = RenderInstancedData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + #ifndef MAGNUM_TARGET_GLES + if(!GL::Context::current().isExtensionSupported()) + CORRADE_SKIP(GL::Extensions::ARB::instanced_arrays::string() + std::string(" is not supported")); + #elif defined(MAGNUM_TARGET_GLES2) + #ifndef MAGNUM_TARGET_WEBGL + if(!GL::Context::current().isExtensionSupported() && + !GL::Context::current().isExtensionSupported() && + !GL::Context::current().isExtensionSupported()) + CORRADE_SKIP("GL_{ANGLE,EXT,NV}_instanced_arrays is not supported"); + #else + if(!GL::Context::current().isExtensionSupported()) + CORRADE_SKIP(GL::Extensions::ANGLE::instanced_arrays::string() + std::string(" is not supported")); + #endif + #endif + + if(!(_manager.loadState("AnyImageImporter") & PluginManager::LoadState::Loaded) || + !(_manager.loadState("TgaImporter") & PluginManager::LoadState::Loaded)) + CORRADE_SKIP("AnyImageImporter / TgaImageImporter plugins not found."); + + GL::Mesh sphere = MeshTools::compile(Primitives::uvSphereSolid(16, 32, + Primitives::UVSphereFlag::TextureCoordinates| + Primitives::UVSphereFlag::Tangents)); + + /* Three spheres, each in a different location, differently rotated to + ensure the normal matrix is properly used as well. */ + struct { + Matrix4 transformation; + Matrix3x3 normal; + Color3 color; + Vector2 textureOffset; + } instanceData[] { + {Matrix4::translation({-1.25f, -1.25f, 0.0f})* + Matrix4::rotationX(90.0_degf), + {}, 0xff3333_rgbf, {0.0f, 0.0f}}, + {Matrix4::translation({ 1.25f, -1.25f, 0.0f})* + Matrix4::rotationY(90.0_degf), + {}, 0x33ff33_rgbf, {1.0f, 0.0f}}, + {Matrix4::translation({ 0.0f, 1.0f, 1.0f})* + Matrix4::rotationZ(90.0_degf), + {}, 0x9999ff_rgbf, {0.5f, 1.0f}} + }; + for(auto& instance: instanceData) + instance.normal = instance.transformation.normalMatrix(); + + sphere + .addVertexBufferInstanced(GL::Buffer{instanceData}, 1, 0, + Phong::TransformationMatrix{}, + Phong::NormalMatrix{}, + Phong::Color3{}, + Phong::TextureOffset{}) + .setInstanceCount(3); + + Containers::Pointer importer = _manager.loadAndInstantiate("AnyImageImporter"); + CORRADE_VERIFY(importer); + + Containers::Optional image; + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(_testDir, "TestFiles/diffuse-texture.tga")) && (image = importer->image2D(0))); + GL::Texture2D diffuse; + diffuse.setMinificationFilter(GL::SamplerFilter::Linear) + .setMagnificationFilter(GL::SamplerFilter::Linear) + .setWrapping(GL::SamplerWrapping::ClampToEdge) + .setStorage(1, TextureFormatRGB, image->size()) + .setSubImage(0, {}, *image); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(_testDir, "TestFiles/normal-texture.tga")) && (image = importer->image2D(0))); + GL::Texture2D normal; + normal.setMinificationFilter(GL::SamplerFilter::Linear) + .setMagnificationFilter(GL::SamplerFilter::Linear) + .setWrapping(GL::SamplerWrapping::ClampToEdge) + .setStorage(1, TextureFormatRGB, image->size()) + .setSubImage(0, {}, *image); + + Phong shader{Phong::Flag::DiffuseTexture| + Phong::Flag::VertexColor| + Phong::Flag::InstancedTransformation| + Phong::Flag::InstancedTextureOffset|data.flags, 2}; + shader + .setLightPositions({{-3.0f, -3.0f, 0.0f}, + { 3.0f, -3.0f, 0.0f}}) + .setTransformationMatrix( + Matrix4::translation(Vector3::zAxis(-1.75f))* + Matrix4::rotationY(-15.0_degf)* + Matrix4::rotationX(15.0_degf)* + Matrix4::scaling(Vector3{0.4f})) + .setNormalMatrix((Matrix4::rotationY(-15.0_degf)* + Matrix4::rotationX(15.0_degf)).normalMatrix()) + .setProjectionMatrix( + Matrix4::perspectiveProjection(60.0_degf, 1.0f, 0.1f, 10.0f)) + .setTextureMatrix(Matrix3::scaling(Vector2{0.5f})) + .bindDiffuseTexture(diffuse) + .setDiffuseColor(0xffff99_rgbf); + + if(data.flags & Phong::Flag::NormalTexture) + shader.bindNormalTexture(normal); + + shader.draw(sphere); + + MAGNUM_VERIFY_NO_GL_ERROR(); + CORRADE_COMPARE_WITH( + /* Dropping the alpha channel, as it's always 1.0 */ + Containers::arrayCast(_framebuffer.read(_framebuffer.viewport(), {PixelFormat::RGBA8Unorm}).pixels()), + Utility::Directory::join({_testDir, "PhongTestFiles", data.file}), + (DebugTools::CompareImageToFile{_manager, data.maxThreshold, data.meanThreshold})); +} + }}}} CORRADE_TEST_MAIN(Magnum::Shaders::Test::PhongGLTest) diff --git a/src/Magnum/Shaders/Test/PhongTest.cpp b/src/Magnum/Shaders/Test/PhongTest.cpp index 4a8a7af54..29d28417e 100644 --- a/src/Magnum/Shaders/Test/PhongTest.cpp +++ b/src/Magnum/Shaders/Test/PhongTest.cpp @@ -39,9 +39,7 @@ struct PhongTest: TestSuite::Tester { void debugFlag(); void debugFlags(); - #ifndef MAGNUM_TARGET_GLES2 - void debugFlagsInstancedObjectId(); - #endif + void debugFlagsSupersets(); }; PhongTest::PhongTest() { @@ -50,10 +48,7 @@ PhongTest::PhongTest() { &PhongTest::debugFlag, &PhongTest::debugFlags, - #ifndef MAGNUM_TARGET_GLES2 - &PhongTest::debugFlagsInstancedObjectId - #endif - }); + &PhongTest::debugFlagsSupersets}); } void PhongTest::constructNoCreate() { @@ -84,16 +79,23 @@ void PhongTest::debugFlags() { CORRADE_COMPARE(out.str(), "Shaders::Phong::Flag::DiffuseTexture|Shaders::Phong::Flag::SpecularTexture Shaders::Phong::Flags{}\n"); } -#ifndef MAGNUM_TARGET_GLES2 -void PhongTest::debugFlagsInstancedObjectId() { - std::ostringstream out; - +void PhongTest::debugFlagsSupersets() { + #ifndef MAGNUM_TARGET_GLES2 /* InstancedObjectId is a superset of ObjectId so only one should be - *printed */ - Debug{&out} << (Phong::Flag::ObjectId|Phong::Flag::InstancedObjectId); - CORRADE_COMPARE(out.str(), "Shaders::Phong::Flag::InstancedObjectId\n"); + printed */ + { + std::ostringstream out; + Debug{&out} << (Phong::Flag::ObjectId|Phong::Flag::InstancedObjectId); + CORRADE_COMPARE(out.str(), "Shaders::Phong::Flag::InstancedObjectId\n"); + } + #endif + + /* InstancedTextureOffset is a superset of TextureTransformation so only + one should be printed */ + std::ostringstream out; + Debug{&out} << (Phong::Flag::InstancedTextureOffset|Phong::Flag::TextureTransformation); + CORRADE_COMPARE(out.str(), "Shaders::Phong::Flag::InstancedTextureOffset\n"); } -#endif }}}} diff --git a/src/Magnum/Shaders/Test/PhongTestFiles/instanced-normal.tga b/src/Magnum/Shaders/Test/PhongTestFiles/instanced-normal.tga new file mode 100644 index 0000000000000000000000000000000000000000..ae238e262e5605e36128abd58051193b0fd8fdda GIT binary patch literal 9469 zcmY*d2Xq|Qb;a(~ZL?*{?#}kv*}e-b8bP9v00|N#L4arkK!5~$w?vVmC`w`xdlyMj zz0i~>SuT<-TlTT+D2}@w+luYPk)1ekY{#i5PMqT;$BCUV_ss%`vggn2-#;_|-Fx4C ze|Dy>&RIA28LAtq3)Ows@AuDbpF`gv{sQ$p5H-^SCdV=Wy+(uxXf!{~IgtFY9TpyDiop z9k8XSgaGZgRf6>&4Z^?FBUo)u7yhiozgIYmnQmg^QVm~9HGJ7<{Fd4L$LZ$pWm)jmx{k-t6^}_!&3cs;t`gMcwt9t&QDuB)Zq(tXHfxj*De_!E&1#y099YwTX z)k85-GXK)(#)hz$*ob$bk>qqr;&YzR6 z{9l#$zbiv5Ksb$AAH(K#7ux-chNQ;jnU)`#E$?O9ewJ(dMW*$~AfK%NeINpsRQ0RT z!fR;p(x(V_g!_7y`@G42D~C7;ND%-E`)}u{y_4gf$Z-2jZrb3+3~t2a)@J#!Jioob zAIyiZcc`~o__s~rD_KJQg8~>}LPB&Eo8MjR@HX}N7W8|YR|19E{3isbiN_(nAUN;LO!H2-S6^vP?uGRnQ3=D(Zg-Z62nH)JYf{VmhO&T7uFB=6o=;lF5U`Ys823Ugn%V}+CF|dTYKiiv30fY|&|Zm`-mGm;|k*z_%9pR)SBhwJ5fg9gFJ4MHQi=URYT6EpF!5 z<@v7^ka~hiRqO*ycdiXEp`*9HJKOeC=$I&fCSLqFJVgL2pY>`}vBGP5=4n0qLL_(l z6jZd;K+_Z{LIx*&Bg?;==XRT1#q=~6U6l-LM5v{7HW_BqQ8p7}a|y06k2ptg7nX(g zdf*c~tB%g9u&6?V&3U1@K>8m`b8i}mfzKsuL06{PWj4Dkz$>>u5N+*6zK!ZPVug={ z%_sHDv$4WQBiZNDxsh<{XvnyzXPzR=*%tvjl6%PleFuG-EX92$4S~6*(p+j%f``WL8FsPusPb#>RodN_7zj8X4gKvKReu4GY4g(kR*}^-iJFR2CB|fy))h9 z%Cu^g4qxLMh>O$*Q|Hj+$bA8Qj}pDVv-?0Kb54yvq9sp<&6`$x68lK@Ry2PH=7I9G zkfyj#80&@PY7__GB2q0?$H{_*fSTrSyHzT-$K| zSj|5qgmn&L;6tHwhh7zxqBGs-G^%Jzr}z5JDL6ujJrD@*R^vw{b)DP4-0AIdcv=Jc z2ju8Me|Tpgyhn~4z|VlbFPJWdo^`_q4#uNG^WxyII2D2OpdZN*h zD2uVYsAsTO(~ktgyC5?l`m_zc;AVGVrQj@x?nbB7=?M&o?q-j?*6Cg1k%uI8+$k;e z1-Izg=aGjlnf$Y2&9bZXp? zA|ymtn25^6gT!sN*5}F{#L5OuTW`*Ch=@wwktjK01vDgqSL1GFDQWLtDJjG5z$zFC z)&dRul;pL5uEY*2(L-|dKrnSNl6^d6TneSH>zQZZrAYRLaOO7M*$skNt4JgO9t7fhm8ts1c)7s=4OHt6Y?=(KNIxXRWB3tQ$LIm{FLmn zWDiJoKRRB!>Z3$~3>;|H6=+<*78T?)?8IvcaNxB#C+oF(XT;1EomM_R0$TnndfgpA9$NmxwC!8Kdg3ZV1Nn94B*NVqk5R2n^BP#ka01Zk2om4 zSw6@En7AE;-fMC2y6pik+2y({dTZ&ru$b&9l zw|--vMj5b{9Q zk|V4~W?Vj-ofVupm#@dFjS*U2rA2}>@9-=D6Bul^++&B~K&%RAa^zkt)7EWdubO_7 zXjrMR*4EjZLoF464n8$+)VxGVknBiK*a@fE-9(_yZm(nPHi5ITEH$w5fKQEv+peO7 z6#8pqF<`w4vcM}b>{(C1#&CFOve^Ze*BzcV!1n~!IK7JnSDANYuy;spFc#_tS-`pr zLSA(pvgQkJ#JYo7Y77m+BoLlv*ftt=_L&#XS~F;ims{CJd^AwSN(!0CCn0%0OEjsAhbT4&ZwV*PA0nA*AZW=F z5Sw_@LpwcH*$E}q0h{eSIxeUIVQ35=o88WMcrguoK@-ulL~t+WT?^*1r%3|b5!UM`5sWP$Ayn$PcEhB>Z|CpxQ1Z^ zHoKeUlbl$fM`r?UBmAO3r%+<($@L=oP)~Sycjl#CnK$<5yW5^Al;0?nUoVzFj+QUJ zmMh%J6<;lsK1RR^Yh4|0BgfvrMn&bny({yvdvmvTns;_*K7L=mbHOcKiCJih$|sAJ zH?5zqV~Tw7L)pSh*cZwlql8?+1_TM295M-BYpfO%=I|7 z$kplOg^9#7Thjms)c_Q;KE1V}zH~K{dp<|(UMmp&*Yl;15a0ZZKxUYaVOn6d)O-bk z;xvq;GdIli#BEG`16%Cec;e_#?BZnd()#3$jVZ{15SV}4%3VN$dCcsSncTA$?OR{~ zgkWA9M8764&IEXzgF}XSHIX`LW-fdX(~~BNQ)BU?YokX7q6l<+ICg0=d2_~iew+E~ z-a>ox6+kjF*Uao=21#)(op}PYAtI*2KAZm_(w8f~g2~gF$1yqfHFinkTq1P}ac?0A zIV*K}eG*ZQ4aKm(f3^O|+UU(44L7!ykdXZYqt*5CSH8)o(yOU9>>4#cXjKDDIhRrKj+1`#h> zR~@&4o{YvHcmV;)Xy)NHku&S!2y|vFeqtoPw>N~i$A{ym*Cnsa6fbOEp37dqgqQ&l zqw&MB#1Shr_+G9#Et7kqP)1xRd8t^wmMdHZPAh{?p@Y-n$z##jA?wu_TVZV zCPez6>XoVFs}C;S-W@!!Qa?A5IystnVSmTw<%19kmqVpk;-N@%eHK;V0PY-Ky1aE06vKod8;c)-Vixh^0E6h3o@d9#Kb6UyE|xCli%31%WAGBP z4;Lj;kH!)YMq&q|h)(UG9=$gfn|KZYXw}H?=?#H6iUrEU!0(*zM;7i`5r)mK%oIL+ zr03N7hHCL3Qj7-HXz|3+lyMT1TZ0#X1W^_&d@^YqHPR={?D=%&I-HD*r_$&3$eyI} z2#AOE-8xBkz|!HqPVzJ>Cm5Q^6;6f1+d|>3 zA$=!ufxO^+sDb`GlJPcWQ+j}$<&cxcq>HEf*UXbDZ-Jx zXuvs-WT-`+YFAMaJ9>g!muUM}>R9wp7f1~%bZ1WxAX`f_sEcU)K^Viz%OzxF9tHw| zHKJfz=u;?R-N4aP$<)zccuO#}3C4)T?z3P_tJ;LD4r`$qEjVrIV3@h5YfHFYh=)0c*77Ne#qF`Txx~eHSvQS@9 z)p{z_A8*%puSjg}j;^e#iwpkFVlo+;Gn7w zD(V;#fhgFc4-qV=la!z!kgUfzB*l2G1G(O>Pwf&MRl}2tKB`1UgQ+cQYP}pE48&La zqP_n33c0x_*xw!+>m!|sO}*)TYqE!i@`u+JA08?nAHL^H*NcgAIn`KBEvOhB z_37?Px;K|tn$Wuwk(GLAMJTvhRr_Hu&7l$_TG(go!HfV>(Bh3x1f z%OCdd=H!uG`@i$K>9<}o z9@vuHKNa6O9No~TkM~5-#=7+3MX~YrIBt2auM1GUO-sBz zi#-b$xSFbtLQaT9Sv;J(U5w;$WK0}VH?F7oEj-$$4eHc{Iz6dOi~<$l;SgGI`(|~+ zq_Tb;AVF0$j6mD?u_K<2MeoDYE_pj$?gpo;==L-Nl9ja;7MN~;A)M7k?zUEEr6gud zKBBXDs1fjbgSWd}z~*jkMpBTD;Et^jL7kpZrpAf&2PKN0AP?r2u0(rvfI<-%#=kV#c3KF@Y^`f#xQt;h&~^S z3dsaSb2m5oR;@q*!Y~MgaO#HjfHxa@cmRi}JO@^z0S0x}tioyDWj&DneRxWid77Qh z0`Q1p%I$6h7^s8}GAN2kf*Q{V@ffSf@C+Vw@Bt(U#fL-C2ckK$COm)|L7a zz#V&nvAPd~)K>NRRxSr}0Pw9?hQ6m`AugfYDBUbdurcDg+*Oym;&5bK?s~WddxtYG zh;a_DPK3A+it&1wl;QdR1LJnHa)8rRG#o|Bq^^#@@F3O%G!#3mf*he!i;F$2UBIw> znY6e=>TCyoAjIl14@M9ZZg&-sB<~_%fEtiObQppVtQ0^|+g(n2><$wOL9mM=Rk;O(Qx#`1PoxpB zw+BQKka9INKpHIRf(Eo|E?P3nwE+Us0>1zPg2N3Kb34OI zj7tIpyqY8wc->_O5J+QpI2aEUk?al;PQt{H9??7tTPf_)Zr}n0RK7*N)7J|W9nMnG zk;{4(wPA4XPq2$t5Kk*0AU!PWv)dhW@$pi`X%}3u#he86n#B5`h!~d(4I@wq+(>il zegOw!VU(71!T?0Op#l_$i9TvjOdHN}Q3&|w<3s$KCpkWd;0%L`s70~k9f*yg_nYSs zRH>za_~T=

T0GpjuqBO6pt$M<6VS!K#jY7NKBU)D!mji8W*H(Z_*+upYzUoe6J6 zv)-ZE7@Lh=j?p_H^q5)6B^VEYp&wEUjzpxn5zD@_QpUOgH|4IL7QOE`z}hS-AtP(4dnZ6|n<%LDt_~%Eb%T{<%ZUy|q2&vUiO)OtvVvlfV{74TEJG9U zxFgUVQRZ>m4K<|Fw2UpRu0L;wH) literal 0 HcmV?d00001 diff --git a/src/Magnum/Shaders/Test/PhongTestFiles/instanced.tga b/src/Magnum/Shaders/Test/PhongTestFiles/instanced.tga new file mode 100644 index 0000000000000000000000000000000000000000..c148e581a8d7a4f80d989bcfb349fd6ebfa9354c GIT binary patch literal 8962 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 literal 0 HcmV?d00001