diff --git a/src/Graphic3d/Graphic3d_RenderingParams.hxx b/src/Graphic3d/Graphic3d_RenderingParams.hxx index d5bd03afc2..5e7a362973 100644 --- a/src/Graphic3d/Graphic3d_RenderingParams.hxx +++ b/src/Graphic3d/Graphic3d_RenderingParams.hxx @@ -53,6 +53,7 @@ public: OitDepthFactor (0.0f), NbMsaaSamples (0), RenderResolutionScale (1.0f), + ToEnableDepthPrepass (Standard_False), // ray tracing parameters IsGlobalIlluminationEnabled (Standard_False), RaytracingDepth (THE_DEFAULT_DEPTH), @@ -105,6 +106,7 @@ public: Standard_Integer NbMsaaSamples; //!< number of MSAA samples (should be within 0..GL_MAX_SAMPLES, power-of-two number), 0 by default Standard_ShortReal RenderResolutionScale; //!< rendering resolution scale factor, 1 by default; //! incompatible with MSAA (e.g. NbMsaaSamples should be set to 0) + Standard_Boolean ToEnableDepthPrepass; //!< enables/disables depth pre-pass, False by default Standard_Boolean IsGlobalIlluminationEnabled; //!< enables/disables global illumination effects (path tracing) Standard_Integer SamplesPerPixel; //!< number of samples per pixel (SPP) diff --git a/src/Graphic3d/Graphic3d_ZLayerSettings.hxx b/src/Graphic3d/Graphic3d_ZLayerSettings.hxx index 8ca091dec4..826ee0aae7 100644 --- a/src/Graphic3d/Graphic3d_ZLayerSettings.hxx +++ b/src/Graphic3d/Graphic3d_ZLayerSettings.hxx @@ -39,7 +39,8 @@ struct Graphic3d_ZLayerSettings myUseEnvironmentTexture (Standard_True), myToEnableDepthTest (Standard_True), myToEnableDepthWrite(Standard_True), - myToClearDepth (Standard_True) {} + myToClearDepth (Standard_True), + myToRenderInDepthPrepass (Standard_True) {} //! Return user-provided name. const TCollection_AsciiString& Name() const { return myName; } @@ -118,6 +119,12 @@ struct Graphic3d_ZLayerSettings //! Set if depth values should be cleared before drawing the layer. void SetClearDepth (const Standard_Boolean theValue) { myToClearDepth = theValue; } + //! Return TRUE if layer should be rendered within depth pre-pass; TRUE by default. + Standard_Boolean ToRenderInDepthPrepass() const { return myToRenderInDepthPrepass; } + + //! Set if layer should be rendered within depth pre-pass. + void SetRenderInDepthPrepass (Standard_Boolean theToRender) { myToRenderInDepthPrepass = theToRender; } + //! Return glPolygonOffset() arguments. const Graphic3d_PolygonOffset& PolygonOffset() const { return myPolygonOffset; } @@ -196,6 +203,7 @@ protected: Standard_Boolean myToEnableDepthTest; //!< option to enable depth test Standard_Boolean myToEnableDepthWrite; //!< option to enable write depth values Standard_Boolean myToClearDepth; //!< option to clear depth values before drawing the layer + Standard_Boolean myToRenderInDepthPrepass;//!< option to render layer within depth pre-pass }; diff --git a/src/OpenGl/OpenGl_CappingAlgo.cxx b/src/OpenGl/OpenGl_CappingAlgo.cxx index 4b2a3e9352..61206dd8b1 100755 --- a/src/OpenGl/OpenGl_CappingAlgo.cxx +++ b/src/OpenGl/OpenGl_CappingAlgo.cxx @@ -73,7 +73,7 @@ namespace aContext->ShaderManager()->UpdateClippingState(); glClear (GL_STENCIL_BUFFER_BIT); - glColorMask (GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + const bool aColorMaskBack = aContext->SetColorMask (false); // override aspects, disable culling theWorkspace->SetAspectFace (&theWorkspace->NoneCulling()); @@ -116,7 +116,7 @@ namespace aContext->ShaderManager()->UpdateClippingState(); // render capping plane using the generated stencil mask - glColorMask (GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + aContext->SetColorMask (aColorMaskBack); if (theWorkspace->UseDepthWrite()) { glDepthMask (GL_TRUE); diff --git a/src/OpenGl/OpenGl_Context.cxx b/src/OpenGl/OpenGl_Context.cxx index 525fbc6305..6fb217a218 100644 --- a/src/OpenGl/OpenGl_Context.cxx +++ b/src/OpenGl/OpenGl_Context.cxx @@ -188,6 +188,7 @@ OpenGl_Context::OpenGl_Context (const Handle(OpenGl_Caps)& theCaps) myReadBuffer (0), myDrawBuffers (1), myDefaultVao (0), + myColorMask (true), myIsGlDebugCtx (Standard_False), myResolution (Graphic3d_RenderingParams::THE_DEFAULT_RESOLUTION), myResolutionRatio (1.0f), @@ -3678,3 +3679,17 @@ void OpenGl_Context::DisableFeatures() const } #endif } + +// ======================================================================= +// function : SetColorMask +// purpose : +// ======================================================================= +bool OpenGl_Context::SetColorMask (bool theToWriteColor) +{ + const GLboolean toWrite = theToWriteColor ? GL_TRUE : GL_FALSE; + glColorMask (toWrite, toWrite, toWrite, toWrite); + + const bool anOldValue = myColorMask; + myColorMask = theToWriteColor; + return anOldValue; +} diff --git a/src/OpenGl/OpenGl_Context.hxx b/src/OpenGl/OpenGl_Context.hxx index 58d150f273..a0e2da762f 100644 --- a/src/OpenGl/OpenGl_Context.hxx +++ b/src/OpenGl/OpenGl_Context.hxx @@ -625,6 +625,12 @@ public: //! @name methods to alter or retrieve current state SetDrawBuffer (theBuffer); } + //! Return cached flag indicating writing into color buffer is enabled or disabled (glColorMask). + bool ColorMask() const { return myColorMask; } + + //! Enable/disable writing into color buffer (wrapper for glColorMask). + Standard_EXPORT bool SetColorMask (bool theToWriteColor); + //! Return back face culling state. bool ToCullBackFaces() const { return myToCullBackFaces; } @@ -906,6 +912,7 @@ private: //! @name fields tracking current state Standard_Integer myReadBuffer; //!< current read buffer OpenGl_DrawBuffers myDrawBuffers; //!< current draw buffers unsigned int myDefaultVao; //!< default Vertex Array Object + Standard_Boolean myColorMask; //!< flag indicating writing into color buffer is enabled or disabled (glColorMask) Standard_Boolean myIsGlDebugCtx; //!< debug context initialization state TCollection_AsciiString myVendor; //!< Graphics Driver's vendor TColStd_PackedMapOfInteger myFilters[6]; //!< messages suppressing filter (for sources from GL_DEBUG_SOURCE_API_ARB to GL_DEBUG_SOURCE_OTHER_ARB) diff --git a/src/OpenGl/OpenGl_LayerList.cxx b/src/OpenGl/OpenGl_LayerList.cxx index ccefa2ed9e..aa361ac2e0 100644 --- a/src/OpenGl/OpenGl_LayerList.cxx +++ b/src/OpenGl/OpenGl_LayerList.cxx @@ -26,6 +26,117 @@ #include +namespace +{ + //! Auxiliary class extending sequence iterator with index. + class OpenGl_IndexedLayerIterator : public OpenGl_SequenceOfLayers::Iterator + { + public: + //! Main constructor. + OpenGl_IndexedLayerIterator (const OpenGl_SequenceOfLayers& theSeq) + : OpenGl_SequenceOfLayers::Iterator (theSeq), + myIndex (theSeq.Lower()) {} + + //! Return index of current position. + Standard_Integer Index() const { return myIndex; } + + //! Move to the next position. + void Next() + { + OpenGl_SequenceOfLayers::Iterator::Next(); + ++myIndex; + } + + private: + Standard_Integer myIndex; + }; + + //! Iterator through layers with filter. + class OpenGl_FilteredIndexedLayerIterator + { + public: + //! Main constructor. + OpenGl_FilteredIndexedLayerIterator (const OpenGl_SequenceOfLayers& theSeq, + Standard_Integer theDefaultLayerIndex, + Standard_Boolean theToDrawImmediate, + OpenGl_LayerFilter theLayersToProcess) + : myIter (theSeq), + myDefaultLayerIndex (theDefaultLayerIndex), + myLayersToProcess (theLayersToProcess), + myToDrawImmediate (theToDrawImmediate) + { + next(); + } + + //! Return true if iterator points to the valid value. + bool More() const { return myIter.More(); } + + //! Return layer at current position. + const OpenGl_Layer& Value() const { return *myIter.Value(); } + + //! Return index of current position. + Standard_Integer Index() const { return myIter.Index(); } + + //! Go to the next item. + void Next() + { + myIter.Next(); + next(); + } + + private: + //! Look for the nearest item passing filters. + void next() + { + for (; myIter.More(); myIter.Next()) + { + if (myIter.Value()->IsImmediate() != myToDrawImmediate) + { + continue; + } + + switch (myLayersToProcess) + { + case OpenGl_LF_All: + { + break; + } + case OpenGl_LF_Upper: + { + if (myIter.Index() <= myDefaultLayerIndex) + { + continue; + } + break; + } + case OpenGl_LF_Bottom: + { + if (myIter.Index() >= myDefaultLayerIndex) + { + continue; + } + break; + } + case OpenGl_LF_Default: + { + if (myIter.Index() != myDefaultLayerIndex) + { + continue; + } + break; + } + } + return; + } + } + private: + OpenGl_IndexedLayerIterator myIter; + Standard_Integer myDefaultLayerIndex; + OpenGl_LayerFilter myLayersToProcess; + Standard_Boolean myToDrawImmediate; + }; +} + //======================================================================= //function : OpenGl_LayerList //purpose : Constructor @@ -223,11 +334,10 @@ void OpenGl_LayerList::RemoveStructure (const OpenGl_Structure* theStructure) } // scan through layers and remove it - Standard_Integer aSeqId = 1; - for (OpenGl_SequenceOfLayers::Iterator anIts (myLayers); anIts.More(); anIts.Next(), ++aSeqId) + for (OpenGl_IndexedLayerIterator anIts (myLayers); anIts.More(); anIts.Next()) { OpenGl_Layer& aLayerEx = *anIts.ChangeValue(); - if (aSeqPos == aSeqId) + if (aSeqPos == anIts.Index()) { continue; } @@ -240,7 +350,7 @@ void OpenGl_LayerList::RemoveStructure (const OpenGl_Structure* theStructure) --myImmediateNbStructures; } - if (aSeqId == myDefaultLayerIndex + if (anIts.Index() == myDefaultLayerIndex && theStructure->IsRaytracable()) { ++myModifStateOfRaytraceable; @@ -299,10 +409,9 @@ void OpenGl_LayerList::ChangeLayer (const OpenGl_Structure* theStructure, } // scan through layers and remove it - Standard_Integer aSeqId = 1; - for (OpenGl_SequenceOfLayers::Iterator anIts (myLayers); anIts.More(); anIts.Next(), ++aSeqId) + for (OpenGl_IndexedLayerIterator anIts (myLayers); anIts.More(); anIts.Next()) { - if (aSeqPos == aSeqId) + if (aSeqPos == anIts.Index()) { continue; } @@ -311,7 +420,7 @@ void OpenGl_LayerList::ChangeLayer (const OpenGl_Structure* theStructure, OpenGl_Layer& aLayerEx = *anIts.ChangeValue(); if (aLayerEx.Remove (theStructure, aPriority, Standard_True)) { - if (aSeqId == myDefaultLayerIndex + if (anIts.Index() == myDefaultLayerIndex && theStructure->IsRaytracable()) { ++myModifStateOfRaytraceable; @@ -356,10 +465,9 @@ void OpenGl_LayerList::ChangePriority (const OpenGl_Structure* theStructure, return; } - Standard_Integer aSeqId = 1; - for (OpenGl_SequenceOfLayers::Iterator anIts (myLayers); anIts.More(); anIts.Next(), ++aSeqId) + for (OpenGl_IndexedLayerIterator anIts (myLayers); anIts.More(); anIts.Next()) { - if (aSeqPos == aSeqId) + if (aSeqPos == anIts.Index()) { continue; } @@ -412,11 +520,12 @@ void OpenGl_LayerList::Render (const Handle(OpenGl_Workspace)& theWorkspace, OpenGl_FrameBuffer* theOitAccumFbo) const { // Remember global settings for glDepth function and write mask. - OpenGl_GlobalLayerSettings aDefaultSettings; + OpenGl_GlobalLayerSettings aPrevSettings; const Handle(OpenGl_Context)& aCtx = theWorkspace->GetGlContext(); - aCtx->core11fwd->glGetIntegerv (GL_DEPTH_FUNC, &aDefaultSettings.DepthFunc); - aCtx->core11fwd->glGetBooleanv (GL_DEPTH_WRITEMASK, &aDefaultSettings.DepthMask); + aCtx->core11fwd->glGetIntegerv (GL_DEPTH_FUNC, &aPrevSettings.DepthFunc); + aCtx->core11fwd->glGetBooleanv (GL_DEPTH_WRITEMASK, &aPrevSettings.DepthMask); + OpenGl_GlobalLayerSettings aDefaultSettings = aPrevSettings; // Two render filters are used to support transparency draw. Opaque filter accepts // only non-transparent OpenGl elements of a layer and counts number of skipped @@ -436,81 +545,114 @@ void OpenGl_LayerList::Render (const Handle(OpenGl_Workspace)& theWorkspace, myTransparentToProcess.Clear(); OpenGl_LayerStack::iterator aStackIter (myTransparentToProcess.Origin()); - Standard_Integer aSeqId = myLayers.Lower(); - bool toClearDepth = false; - for (OpenGl_SequenceOfLayers::Iterator aLayerIter (myLayers); aLayerIter.More(); aLayerIter.Next(), ++aSeqId) + Standard_Integer aClearDepthLayerPrev = -1, aClearDepthLayer = -1; + const bool toPerformDepthPrepass = theWorkspace->View()->RenderingParams().ToEnableDepthPrepass + && aPrevSettings.DepthMask == GL_TRUE; + for (OpenGl_FilteredIndexedLayerIterator aLayerIterStart (myLayers, myDefaultLayerIndex, theToDrawImmediate, theLayersToProcess); aLayerIterStart.More();) { - if (theLayersToProcess == OpenGl_LF_Bottom) + bool hasSkippedDepthLayers = false; + for (int aPassIter = toPerformDepthPrepass ? 0 : 2; aPassIter < 3; ++aPassIter) { - if (aSeqId >= myDefaultLayerIndex) continue; - } - else if (theLayersToProcess == OpenGl_LF_Upper) - { - if (aSeqId <= myDefaultLayerIndex) continue; - } - else if (theLayersToProcess == OpenGl_LF_Default) - { - if (aSeqId != myDefaultLayerIndex) continue; - } - - const OpenGl_Layer& aLayer = *aLayerIter.Value(); - if (aLayer.IsImmediate() != theToDrawImmediate) - { - continue; - } - else if (aLayer.NbStructures() < 1) - { - // Make sure to clear depth of previous layers even if layer has no structures. - toClearDepth = toClearDepth || aLayer.LayerSettings().ToClearDepth(); - continue; - } - - // At this point the depth buffer may be set to clear by - // previous configuration of layers or configuration of the - // current layer. Additional rendering pass to handle transparent - // elements of recently drawn layers require use of current depth - // buffer so we put remaining layers for processing as one bunch before - // erasing the depth buffer. - if (toClearDepth - || aLayer.LayerSettings().ToClearDepth()) - { - if (!myTransparentToProcess.IsEmpty()) + if (aPassIter == 0) { - renderTransparent (theWorkspace, aStackIter, aDefaultSettings, theReadDrawFbo, theOitAccumFbo); + aCtx->SetColorMask (false); + aDefaultSettings.DepthFunc = aPrevSettings.DepthFunc; + aDefaultSettings.DepthMask = GL_TRUE; + } + else if (aPassIter == 1) + { + if (!hasSkippedDepthLayers) + { + continue; + } + aCtx->SetColorMask (true); + aDefaultSettings = aPrevSettings; + } + else if (aPassIter == 2) + { + aCtx->SetColorMask (true); + if (toPerformDepthPrepass) + { + aDefaultSettings.DepthFunc = GL_EQUAL; + aDefaultSettings.DepthMask = GL_FALSE; + } } - toClearDepth = false; + OpenGl_FilteredIndexedLayerIterator aLayerIter (aLayerIterStart); + for (; aLayerIter.More(); aLayerIter.Next()) + { + const OpenGl_Layer& aLayer = aLayerIter.Value(); + + // make sure to clear depth of previous layers even if layer has no structures + if (aLayer.LayerSettings().ToClearDepth()) + { + aClearDepthLayer = aLayerIter.Index(); + } + if (aLayer.NbStructures() < 1) + { + continue; + } + else if (aClearDepthLayer > aClearDepthLayerPrev) + { + // At this point the depth buffer may be set to clear by previous configuration of layers or configuration of the current layer. + // Additional rendering pass to handle transparent elements of recently drawn layers require use of current depth + // buffer so we put remaining layers for processing as one bunch before erasing the depth buffer. + if (aPassIter == 2) + { + aLayerIterStart = aLayerIter; + } + else + { + aClearDepthLayer = -1; + } + break; + } + else if (aPassIter == 0 + && !aLayer.LayerSettings().ToRenderInDepthPrepass()) + { + hasSkippedDepthLayers = true; + continue; + } + else if (aPassIter == 1 + && aLayer.LayerSettings().ToRenderInDepthPrepass()) + { + continue; + } + + // Render opaque OpenGl elements of a layer and count the number of skipped. + // If a layer has skipped (e.g. transparent) elements it should be added into + // the transparency post-processing stack. + myRenderOpaqueFilter->SetSkippedCounter (0); + + aLayer.Render (theWorkspace, aDefaultSettings); + + if (aPassIter != 0 + && myRenderOpaqueFilter->NbSkipped() > 0) + { + myTransparentToProcess.Push (&aLayer); + } + } + if (aPassIter == 2 + && !aLayerIter.More()) + { + aLayerIterStart = aLayerIter; + } + } + + if (!myTransparentToProcess.IsEmpty()) + { + renderTransparent (theWorkspace, aStackIter, aPrevSettings, theReadDrawFbo, theOitAccumFbo); + } + if (aClearDepthLayer > aClearDepthLayerPrev) + { + aClearDepthLayerPrev = aClearDepthLayer; glDepthMask (GL_TRUE); glClear (GL_DEPTH_BUFFER_BIT); } - - // Render opaque OpenGl elements of a layer and count the number of skipped. - // If a layer has skipped (e.g. transparent) elements it should be added into - // the transparency post-processing stack. - myRenderOpaqueFilter->SetSkippedCounter (0); - - aLayer.Render (theWorkspace, aDefaultSettings); - - if (myRenderOpaqueFilter->NbSkipped() > 0) - { - myTransparentToProcess.Push (&aLayer); - } } - // Before finishing process the remaining collected layers with transparency. - if (!myTransparentToProcess.IsEmpty()) - { - renderTransparent (theWorkspace, aStackIter, aDefaultSettings, theReadDrawFbo, theOitAccumFbo); - } - - if (toClearDepth) - { - glDepthMask (GL_TRUE); - glClear (GL_DEPTH_BUFFER_BIT); - } - - aCtx->core11fwd->glDepthMask (aDefaultSettings.DepthMask); - aCtx->core11fwd->glDepthFunc (aDefaultSettings.DepthFunc); + aCtx->core11fwd->glDepthMask (aPrevSettings.DepthMask); + aCtx->core11fwd->glDepthFunc (aPrevSettings.DepthFunc); theWorkspace->SetRenderFilter (aPrevFilter); } diff --git a/src/OpenGl/OpenGl_PrimitiveArray.cxx b/src/OpenGl/OpenGl_PrimitiveArray.cxx index c2ca7b9e83..84030e15bf 100644 --- a/src/OpenGl/OpenGl_PrimitiveArray.cxx +++ b/src/OpenGl/OpenGl_PrimitiveArray.cxx @@ -736,7 +736,8 @@ void OpenGl_PrimitiveArray::Render (const Handle(OpenGl_Workspace)& theWorkspace && myVboAttribs->HasColorAttribute(); const Standard_Boolean isLightOn = !anAspectFace->IsNoLighting() && !myVboAttribs.IsNull() - && myVboAttribs->HasNormalAttribute(); + && myVboAttribs->HasNormalAttribute() + && aCtx->ColorMask(); // Temporarily disable environment mapping Handle(OpenGl_TextureSet) aTextureBack; diff --git a/src/OpenGl/OpenGl_Text.cxx b/src/OpenGl/OpenGl_Text.cxx index fd9757f44d..5bfd924b9b 100644 --- a/src/OpenGl/OpenGl_Text.cxx +++ b/src/OpenGl/OpenGl_Text.cxx @@ -958,7 +958,7 @@ void OpenGl_Text::render (const Handle(OpenGl_Context)& theCtx, glDisable (GL_ALPHA_TEST); } #endif - glColorMask (GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + const bool aColorMaskBack = theCtx->SetColorMask (false); glClear (GL_STENCIL_BUFFER_BIT); glEnable (GL_STENCIL_TEST); @@ -969,7 +969,7 @@ void OpenGl_Text::render (const Handle(OpenGl_Context)& theCtx, glStencilFunc (GL_ALWAYS, 0, 0xFF); - glColorMask (GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + theCtx->SetColorMask (aColorMaskBack); } // reset OpenGL state diff --git a/src/ViewerTest/ViewerTest_ViewerCommands.cxx b/src/ViewerTest/ViewerTest_ViewerCommands.cxx index fcc5c5cce1..a8f65a72a5 100644 --- a/src/ViewerTest/ViewerTest_ViewerCommands.cxx +++ b/src/ViewerTest/ViewerTest_ViewerCommands.cxx @@ -9843,6 +9843,7 @@ static Standard_Integer VRenderParams (Draw_Interpretor& theDI, case V3d_GOURAUD: theDI << "gouraud"; break; case V3d_PHONG: theDI << "phong"; break; } + theDI << "depth pre-pass: " << (aParams.ToEnableDepthPrepass ? "on" : "off") << "\n"; theDI << "\n"; return 0; } @@ -9974,6 +9975,20 @@ static Standard_Integer VRenderParams (Draw_Interpretor& theDI, return 1; } } + else if (aFlag == "-depthprepass") + { + if (toPrint) + { + theDI << (aParams.ToEnableDepthPrepass ? "on " : "off "); + continue; + } + aParams.ToEnableDepthPrepass = Standard_True; + if (anArgIter + 1 < theArgNb + && ViewerTest::ParseOnOff (theArgVec[anArgIter + 1], aParams.ToEnableDepthPrepass)) + { + ++anArgIter; + } + } else if (aFlag == "-rendscale" || aFlag == "-renderscale" || aFlag == "-renderresolutionscale") @@ -11946,6 +11961,7 @@ void ViewerTest::ViewerCommands(Draw_Interpretor& theCommands) "\n '-raster' Disables GPU ray-tracing" "\n '-msaa 0..4' Specifies number of samples for MSAA" "\n '-oit off|0.0-1.0' Enables/disables OIT and sets depth weight factor" + "\n '-depthPrePass on|off' Enables/disables depth pre-pass" "\n '-rendScale value Rendering resolution scale factor" "\n '-rayTrace' Enables GPU ray-tracing" "\n '-rayDepth 0..10' Defines maximum ray-tracing depth" diff --git a/tests/v3d/glsl/depthprepass b/tests/v3d/glsl/depthprepass new file mode 100644 index 0000000000..be106f0df8 --- /dev/null +++ b/tests/v3d/glsl/depthprepass @@ -0,0 +1,27 @@ +puts "========" +puts "0029300: Visualization, TKOpenGl - provide depth pre-pass option" +puts "========" + +vclear +vclose ALL +vinit View1 + +# display objects +psphere s 1 +box b 1 2 3 +vaxo +vdisplay -dispMode 1 s b +vaspects b -setColor RED -setTransparency 0.5 +vfit +vzbufftrihedron +vmoveto 150 250 +vcaps -ffp 0 +vrenderparams -shadingModel phong + +vrenderparams -depthPrePass off +vdump $::imagedir/${::casename}_1.png + +vrenderparams -depthPrePass on +vdump $::imagedir/${::casename}_2.png + +if { [diffimage $::imagedir/${::casename}_1.png $::imagedir/${::casename}_2.png 0 0 0 $::imagedir/${::casename}_diff.png] != 0 } { puts "Error: images differ" }