From 662bea239c25400fb7a3d693abbeabbea6fe32ed Mon Sep 17 00:00:00 2001 From: Julien Valentin Date: Wed, 3 Jan 2018 21:22:38 +0100 Subject: [PATCH] add GLSampler as Texture Property and a simple example mixing (add) 2 textures --- examples/CMakeLists.txt | 2 +- examples/osgsampler/CMakeLists.txt | 4 + examples/osgsampler/osgSampler.cpp | 208 +++++++++++++++++++++++ include/osg/Sampler | 118 +++++++++++++ include/osg/StateAttribute | 3 +- include/osg/Texture | 3 +- src/osg/CMakeLists.txt | 3 +- src/osg/Sampler.cpp | 256 +++++++++++++++++++++++++++++ 8 files changed, 593 insertions(+), 4 deletions(-) create mode 100644 examples/osgsampler/CMakeLists.txt create mode 100644 examples/osgsampler/osgSampler.cpp create mode 100644 include/osg/Sampler create mode 100644 src/osg/Sampler.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 9d54ac9ca..4edb8dc39 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -99,6 +99,7 @@ IF(DYNAMIC_OPENSCENEGRAPH) ADD_SUBDIRECTORY(osgreflect) ADD_SUBDIRECTORY(osgrobot) ADD_SUBDIRECTORY(osgSSBO) + ADD_SUBDIRECTORY(osgsampler) ADD_SUBDIRECTORY(osgscalarbar) ADD_SUBDIRECTORY(osgscribe) ADD_SUBDIRECTORY(osgsequence) @@ -253,4 +254,3 @@ ELSE(DYNAMIC_OPENSCENEGRAPH) ENDIF(DYNAMIC_OPENSCENEGRAPH) ENDIF(ANDROID) - diff --git a/examples/osgsampler/CMakeLists.txt b/examples/osgsampler/CMakeLists.txt new file mode 100644 index 000000000..126cd2309 --- /dev/null +++ b/examples/osgsampler/CMakeLists.txt @@ -0,0 +1,4 @@ +SET(TARGET_SRC osgSampler.cpp ) +SET(TARGET_ADDED_LIBRARIES osgManipulator) + +SETUP_EXAMPLE(osgsampler) diff --git a/examples/osgsampler/osgSampler.cpp b/examples/osgsampler/osgSampler.cpp new file mode 100644 index 000000000..c789bb7c9 --- /dev/null +++ b/examples/osgsampler/osgSampler.cpp @@ -0,0 +1,208 @@ +/* -*-c++-*- + * Copyright (C) 2017 Julien Valentin + * + * This library is open source and may be redistributed and/or modified under + * the terms of the OpenSceneGraph Public License (OSGPL) version 0.0 or + * (at your option) any later version. The full license is in LICENSE file + * included with this distribution, and on the openscenegraph.org website. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * OpenSceneGraph Public License for more details. +*/ + + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/// add two composite texture color +static const char* fragSource = +{ + "uniform sampler2D tex1;\n" + "uniform sampler2D tex2;\n" + "void main(){\n" + " vec4 color1 = texture2D(tex1,gl_TexCoord[0].st);\n" + " vec4 color2 = texture2D(tex2,gl_TexCoord[0].st);\n" + " gl_FragColor = vec4(color1.xyz+color2.xyz,1.0);\n" + "}\n" +}; + + +class KeyboardEventHandler : public osgGA::GUIEventHandler +{ +public: + KeyboardEventHandler(osg::Sampler* tessInnerU, osg::Sampler* tessOuterU,osg::StateSet* ss): + _sampler1(tessInnerU), + _sampler2(tessOuterU),_ss(ss){} + + virtual bool handle(const osgGA::GUIEventAdapter& ea,osgGA::GUIActionAdapter& gaa) + { + osg::Texture:: FilterMode newone=osg::Texture::NEAREST; + if(ea.getEventType()==osgGA::GUIEventAdapter::KEYDOWN) + { + switch (ea.getKey()) + { + case osgGA::GUIEventAdapter::KEY_A: + if(_sampler1->getFilter(osg::Texture::MAG_FILTER)==osg::Texture::NEAREST) + newone=osg::Texture::LINEAR; + _sampler1->setFilter(osg::Texture::MAG_FILTER,newone); + _sampler1->setFilter(osg::Texture::MIN_FILTER,newone); + return true; + case osgGA::GUIEventAdapter::KEY_B: + if(_sampler2->getFilter(osg::Texture::MAG_FILTER)==osg::Texture::NEAREST) + newone=osg::Texture::LINEAR; + _sampler2->setFilter(osg::Texture::MAG_FILTER,newone); + _sampler2->setFilter(osg::Texture::MIN_FILTER,newone); + return true; + + } + } + return osgGA::GUIEventHandler::handle(ea, gaa); + } + +private: + osg::Sampler* _sampler1; + osg::Sampler* _sampler2; + osg::StateSet* _ss; + +}; + +int main(int argc, char* argv[]) +{ + osg::ArgumentParser arguments(&argc,argv); + arguments.getApplicationUsage()->setApplicationName(arguments.getApplicationName()); + arguments.getApplicationUsage()->setDescription(arguments.getApplicationName()+" is the standard OpenSceneGraph example for GL3 sampler."); + osg::ApplicationUsage::UsageMap kmap; + kmap["a"]="swap Linear/Nearest filtering on first texture"; + kmap["b"]="swap Linear/Nearest filtering on second texture"; + arguments.getApplicationUsage()->setKeyboardMouseBindings(kmap); + + osgViewer::Viewer viewer(arguments); + osg::ref_ptr program = new osg::Program(); + program->addShader(new osg::Shader(osg::Shader::FRAGMENT,fragSource)); + + osg::ref_ptr loadedModel = osgDB::readRefNodeFiles(arguments); + osg::ref_ptr geode ; + if (!loadedModel) + { + geode = new osg::Geode; + ((osg::Geode*)geode.get())->addDrawable(osg::createTexturedQuadGeometry(osg::Vec3(0,0,0),osg::Vec3(1,0,0),osg::Vec3(0,0,1) )); + } + else + { + geode=loadedModel; + } + + osg::ref_ptr< osg::Texture2D> tex1, tex2; + osg::ref_ptr sampler1, sampler2; + + tex1=new osg::Texture2D(); + tex2=new osg::Texture2D(); + + sampler1=new osg::Sampler(); + sampler2=new osg::Sampler(); + + osg::Vec2ui resolution(4,4); + { + ///first texture//NO RED + unsigned char *data = new unsigned char[(resolution.x()* resolution.y()) * 4]; + unsigned char * ptr=data; + unsigned int sw=0,cptx=0; + while (ptr != data + sizeof(unsigned char)*(resolution.x()* resolution.y()) * 4) + { + if(sw==1) + { + *ptr++=0; + *ptr++=0xff; + *ptr++=0xff; + *ptr++=0xff; + } + else + { + *ptr++=0; + *ptr++=0; + *ptr++=0; + *ptr++=0xff; + } + if(++cptxim = new osg::Image(); + im->setImage(resolution.x(), resolution.y(), 1,GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE, data, osg::Image::USE_NEW_DELETE); + im->dirty(); + tex1->setImage(im); + } + { + ///second texture// RED ONLY + unsigned char *data = new unsigned char[(resolution.x()* resolution.y()) * 4]; + unsigned char * ptr=data; + unsigned int sw=0,cptx=0; + while (ptr != data + sizeof(unsigned char)*(resolution.x()* resolution.y()) * 4) + { + if(sw==1) + { + *ptr++=255; + *ptr++=0; + *ptr++=0; + *ptr++=255; + } + else + { + *ptr++=0; + *ptr++=0; + *ptr++=0; + *ptr++=255; + } + if(++cptxim = new osg::Image(); + im->setImage(resolution.x(), resolution.y(), 1,GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE, data, osg::Image::USE_NEW_DELETE); + im->dirty(); + tex2->setImage(im); + } + ///Overrided Filtering setup + tex1->setFilter(osg::Texture::MAG_FILTER,osg::Texture::NEAREST); + tex1->setFilter(osg::Texture::MIN_FILTER,osg::Texture::NEAREST); + + tex2->setFilter(osg::Texture::MAG_FILTER,osg::Texture::NEAREST); + tex2->setFilter(osg::Texture::MIN_FILTER,osg::Texture::NEAREST); + + ///Filter Override samplers setup + sampler1->setFilter(osg::Texture::MAG_FILTER,osg::Texture::LINEAR); + sampler1->setFilter(osg::Texture::MIN_FILTER,osg::Texture::LINEAR); + + sampler2->setFilter(osg::Texture::MAG_FILTER,osg::Texture::LINEAR); + sampler2->setFilter(osg::Texture::MIN_FILTER,osg::Texture::LINEAR); + + + osg::StateSet *ss; + ss = geode->getOrCreateStateSet(); + + ss->setTextureAttribute(0,tex1,osg::StateAttribute::ON); + ss->setTextureAttribute(1,tex2,osg::StateAttribute::ON); + ss->setTextureAttribute(0,sampler1,osg::StateAttribute::ON); + ss->setTextureAttribute(1,sampler2,osg::StateAttribute::ON); + + ss->addUniform(new osg::Uniform("tex1",(int)0)); + ss->addUniform(new osg::Uniform("tex2",(int)1)); + ss->setAttribute(program.get()); + + + viewer.addEventHandler(new KeyboardEventHandler(sampler1, sampler2,ss)); + viewer.addEventHandler(new osgViewer::StatsHandler); + + viewer.addEventHandler(new osgViewer::HelpHandler(arguments.getApplicationUsage())); + + viewer.setSceneData(geode.get()); + return viewer.run(); +} diff --git a/include/osg/Sampler b/include/osg/Sampler new file mode 100644 index 000000000..1ffdda1ab --- /dev/null +++ b/include/osg/Sampler @@ -0,0 +1,118 @@ +/* -*-c++-*- + * Copyright (C) 2017 Julien Valentin + * + * This library is open source and may be redistributed and/or modified under + * the terms of the OpenSceneGraph Public License (OSGPL) version 0.0 or + * (at your option) any later version. The full license is in LICENSE file + * included with this distribution, and on the openscenegraph.org website. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * OpenSceneGraph Public License for more details. +*/ +#ifndef OSG_SAMPLER_H +#define OSG_SAMPLER_H 1 + +#include + +namespace osg{ +/** OpenGL Sampler + * OpenGL 3.3 required + * https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_sampler_objects.txt + * State Attribute controllig sampling instead of Texture + * Sampler is prioritary over Texture sample parameter (don't play with both) +*/ + + +class OSG_EXPORT Sampler : public osg::StateAttribute +{ + public: + Sampler(); + + /** Copy constructor using CopyOp to manage deep vs shallow copy. */ + Sampler(const Sampler& text,const CopyOp& copyop=CopyOp::SHALLOW_COPY); + + META_StateAttribute(osg,Sampler,SAMPLER) + + virtual bool isTextureAttribute() const { return true; } + + /** Sets the texture wrap mode. */ + void setWrap(Texture::WrapParameter which, Texture::WrapMode wrap); + + /** Gets the texture wrap mode. */ + Texture::WrapMode getWrap(Texture::WrapParameter which) const; + + /** Sets the texture filter mode. */ + void setFilter(Texture::FilterParameter which, Texture::FilterMode filter); + + /** Gets the texture filter mode. */ + Texture::FilterMode getFilter(Texture::FilterParameter which) const; + + /** Sets shadow texture comparison function. */ + void setShadowCompareFunc(Texture::ShadowCompareFunc func); + Texture::ShadowCompareFunc getShadowCompareFunc() const { return _shadow_compare_func; } + + /** Sets shadow texture mode after comparison. */ + void setShadowTextureMode(Texture::ShadowTextureMode mode); + Texture::ShadowTextureMode getShadowTextureMode() const { return _shadow_texture_mode; } + + /** Sets the border color. Only used when wrap mode is CLAMP_TO_BORDER. + * The border color will be casted to the appropriate type to match the + * internal pixel format of the texture. */ + void setBorderColor(const Vec4d& color); + + /** Gets the border color. */ + const Vec4d& getBorderColor() const { return _borderColor; } + + /** Sets the maximum anisotropy value, default value is 1.0 for no + * anisotropic filtering. If hardware does not support anisotropic + * filtering, use normal filtering (equivalent to a max anisotropy + * value of 1.0. Valid range is 1.0f upwards. The maximum value + * depends on the graphics system. */ + void setMaxAnisotropy(float anis); + + /** Gets the maximum anisotropy value. */ + inline float getMaxAnisotropy() const { return _maxAnisotropy; } + + void setMinLOD(float anis); + + /** Gets the maximum anisotropy value. */ + inline float getMinLOD() const { return _minlod; } + + void setMaxLOD(float anis); + + /** Gets the maximum anisotropy value. */ + inline float getMaxLOD() const { return _maxlod; } + + void setLODBias(float anis); + + /** Gets the maximum anisotropy value. */ + inline float getLODBias() const { return _lodbias; } + + virtual void apply(State& state) const; + + virtual void compileGLObjects(State&) const; + + /** release state's SamplerObject **/ + virtual void releaseGLObjects(State* state=0) const; + + virtual int compare(const StateAttribute& sa) const; + + protected: + Texture::WrapMode _wrap_s; + Texture::WrapMode _wrap_t; + Texture::WrapMode _wrap_r; + Texture::ShadowCompareFunc _shadow_compare_func; + Texture::ShadowTextureMode _shadow_texture_mode; + Vec4d _borderColor; + + Texture::FilterMode _min_filter; + Texture::FilterMode _mag_filter; + float _maxAnisotropy, _minlod, _maxlod, _lodbias; + + mutable buffered_value _PCsampler; + mutable buffered_value _PCdirtyflags; +}; +} +#endif diff --git a/include/osg/StateAttribute b/include/osg/StateAttribute index 06b3a3638..0cb3270a5 100644 --- a/include/osg/StateAttribute +++ b/include/osg/StateAttribute @@ -207,8 +207,9 @@ class OSG_EXPORT StateAttribute : public Object VIEWPORTINDEXED, DEPTHRANGEINDEXED, SCISSORINDEXED, - + BINDIMAGETEXTURE, + SAMPLER, CAPABILITY = 100 }; diff --git a/include/osg/Texture b/include/osg/Texture index f1a7203a3..5fc6977b5 100644 --- a/include/osg/Texture +++ b/include/osg/Texture @@ -699,7 +699,8 @@ class OSG_EXPORT Texture : public osg::StateAttribute enum ShadowTextureMode { LUMINANCE = GL_LUMINANCE, INTENSITY = GL_INTENSITY, - ALPHA = GL_ALPHA + ALPHA = GL_ALPHA, + NONE = GL_NONE }; /** Sets shadow texture mode after comparison. */ diff --git a/src/osg/CMakeLists.txt b/src/osg/CMakeLists.txt index c1e0fc1e4..b2a60d50f 100644 --- a/src/osg/CMakeLists.txt +++ b/src/osg/CMakeLists.txt @@ -148,6 +148,7 @@ SET(TARGET_H ${HEADER_PATH}/ref_ptr ${HEADER_PATH}/RenderInfo ${HEADER_PATH}/SampleMaski + ${HEADER_PATH}/Sampler ${HEADER_PATH}/Scissor ${HEADER_PATH}/ScissorIndexed ${HEADER_PATH}/ScriptEngine @@ -355,6 +356,7 @@ SET(TARGET_SRC Quat.cpp Referenced.cpp SampleMaski.cpp + Sampler.cpp Scissor.cpp ScissorIndexed.cpp ScriptEngine.cpp @@ -454,4 +456,3 @@ SET(TARGET_EXTERNAL_LIBRARIES #INCLUDE(ModuleInstall OPTIONAL) SETUP_LIBRARY(${LIB_NAME}) - diff --git a/src/osg/Sampler.cpp b/src/osg/Sampler.cpp new file mode 100644 index 000000000..3f83debd2 --- /dev/null +++ b/src/osg/Sampler.cpp @@ -0,0 +1,256 @@ +/* -*-c++-*- + * Copyright (C) 2017 Julien Valentin + * + * This library is open source and may be redistributed and/or modified under + * the terms of the OpenSceneGraph Public License (OSGPL) version 0.0 or + * (at your option) any later version. The full license is in LICENSE file + * included with this distribution, and on the openscenegraph.org website. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * OpenSceneGraph Public License for more details. +*/ + +#include +#include + +#ifndef GL_TEXTURE_MIN_LOD +#define GL_TEXTURE_MIN_LOD 0x813A +#endif + +#ifndef GL_TEXTURE_MAX_LOD +#define GL_TEXTURE_MAX_LOD 0x813B +#endif + +#ifndef GL_TEXTURE_WRAP_R +#define GL_TEXTURE_WRAP_R 0x2804 +#endif + +using namespace osg; + +Sampler::Sampler(): StateAttribute(), + _wrap_s(Texture::CLAMP), + _wrap_t(Texture::CLAMP), + _wrap_r(Texture::CLAMP), + _shadow_compare_func(Texture::LEQUAL), + _shadow_texture_mode(Texture::NONE), + _min_filter(Texture::LINEAR_MIPMAP_LINEAR), // trilinear + _mag_filter(Texture::LINEAR), + _maxAnisotropy(1.0f), + _minlod(0.0f), + _maxlod(-1.0f), + _lodbias(0.0f) +{ + _PCdirtyflags.setAllElementsTo(true); + _PCsampler.setAllElementsTo(0); +} +Sampler::Sampler(const Sampler& sampler,const CopyOp ©op ):StateAttribute(sampler,copyop), + _wrap_s(sampler._wrap_s), + _wrap_t(sampler._wrap_t), + _wrap_r(sampler._wrap_r), + _shadow_compare_func(sampler._shadow_compare_func), + _shadow_texture_mode(sampler._shadow_texture_mode), + _min_filter(sampler._min_filter), + _mag_filter(sampler._mag_filter), + _maxAnisotropy(sampler._maxAnisotropy), + _minlod(sampler._minlod), + _maxlod(sampler._maxlod), + _lodbias(sampler._lodbias) +{ + _PCdirtyflags.setAllElementsTo(true); + _PCsampler.setAllElementsTo(0); +} + +void Sampler::setWrap(Texture::WrapParameter which, Texture::WrapMode wrap) +{ + switch( which ) + { + case Texture::WRAP_S : _wrap_s = wrap; _PCdirtyflags.setAllElementsTo(true); break; + case Texture::WRAP_T : _wrap_t = wrap; _PCdirtyflags.setAllElementsTo(true); break; + case Texture::WRAP_R : _wrap_r = wrap; _PCdirtyflags.setAllElementsTo(true); break; + default : OSG_WARN<<"Error: invalid 'which' passed Sampler::setWrap("<<(unsigned int)which<<","<<(unsigned int)wrap<<")"<(); + GLuint samplerobject = _PCsampler[contextID]; + if(samplerobject==0) + { + extensions->glGenSamplers(1,&_PCsampler[contextID]); + samplerobject = _PCsampler[contextID]; + } + + Texture::WrapMode ws = _wrap_s, wt = _wrap_t, wr = _wrap_r; + + // GL_IBM_texture_mirrored_repeat, fall-back REPEAT + if (!extensions->isTextureMirroredRepeatSupported) + { + if (ws == Texture::MIRROR) + ws = Texture::REPEAT; + if (wt == Texture::MIRROR) + wt = Texture::REPEAT; + if (wr == Texture::MIRROR) + wr = Texture::REPEAT; + } + + // GL_EXT_texture_edge_clamp, fall-back CLAMP + if (!extensions->isTextureEdgeClampSupported) + { + if (ws == Texture::CLAMP_TO_EDGE) + ws = Texture::CLAMP; + if (wt == Texture::CLAMP_TO_EDGE) + wt = Texture::CLAMP; + if (wr == Texture::CLAMP_TO_EDGE) + wr = Texture::CLAMP; + } + + if(!extensions->isTextureBorderClampSupported) + { + if(ws == Texture::CLAMP_TO_BORDER) + ws = Texture::CLAMP; + if(wt == Texture::CLAMP_TO_BORDER) + wt = Texture::CLAMP; + if(wr == Texture::CLAMP_TO_BORDER) + wr = Texture::CLAMP; + } + + #if defined(OSG_GLES1_AVAILABLE) || defined(OSG_GLES2_AVAILABLE) || defined(OSG_GL3_AVAILABLE) + if (ws == Texture::CLAMP) ws = Texture::CLAMP_TO_EDGE; + if (wt == Texture::CLAMP) wt = Texture::CLAMP_TO_EDGE; + if (wr == Texture::CLAMP) wr = Texture::CLAMP_TO_EDGE; + #endif + + extensions->glSamplerParameteri( samplerobject, GL_TEXTURE_WRAP_S, ws ); + extensions->glSamplerParameteri( samplerobject, GL_TEXTURE_WRAP_T, wt ); + extensions->glSamplerParameteri( samplerobject, GL_TEXTURE_WRAP_R, wr ); + + + extensions->glSamplerParameteri( samplerobject, GL_TEXTURE_MIN_FILTER, _min_filter); + extensions->glSamplerParameteri( samplerobject, GL_TEXTURE_MAG_FILTER, _mag_filter); + + if (extensions->isTextureBorderClampSupported) + { + + #ifndef GL_TEXTURE_BORDER_COLOR + #define GL_TEXTURE_BORDER_COLOR 0x1004 + #endif + + GLfloat color[4] = {(GLfloat)_borderColor.r(), (GLfloat)_borderColor.g(), (GLfloat)_borderColor.b(), (GLfloat)_borderColor.a()}; + extensions->glSamplerParameterfv(samplerobject, GL_TEXTURE_BORDER_COLOR, color); + + } + + extensions->glSamplerParameteri(samplerobject, GL_TEXTURE_COMPARE_MODE, _shadow_texture_mode); + extensions->glSamplerParameteri(samplerobject, GL_TEXTURE_COMPARE_FUNC, _shadow_compare_func); + + if (extensions->isTextureFilterAnisotropicSupported ) + { + // note, GL_TEXTURE_MAX_ANISOTROPY_EXT will either be defined + // by gl.h (or via glext.h) or by include/osg/Texture. + extensions->glSamplerParameterf(samplerobject, GL_TEXTURE_MAX_ANISOTROPY_EXT, _maxAnisotropy); + } + + if( _maxlod - _minlod > 0){ // if range is valid + extensions->glSamplerParameterf(samplerobject, GL_TEXTURE_MIN_LOD, _minlod); + extensions->glSamplerParameterf(samplerobject, GL_TEXTURE_MAX_LOD, _maxlod); + } + + extensions->glSamplerParameterf(samplerobject, GL_TEXTURE_LOD_BIAS, _lodbias); + _PCdirtyflags[contextID]=false; + } +} + +/** bind SamplerObject **/ +void Sampler::apply(State&state) const +{ + + unsigned int contextID = state.getContextID(); + if( _PCdirtyflags[contextID] ) + compileGLObjects(state); + state.get()->glBindSampler( state.getActiveTextureUnit(), _PCsampler[contextID] ); + +} + +void Sampler::releaseGLObjects(State* state) const +{ + if(state) + { + unsigned int contextID=state->getContextID(); + state->get()->glDeleteSamplers(1,&_PCsampler[contextID]); + } +} +int Sampler::compare(const StateAttribute& sa) const{ + COMPARE_StateAttribute_Types(Sampler,sa) + COMPARE_StateAttribute_Parameter(_wrap_t) + COMPARE_StateAttribute_Parameter(_wrap_r) + COMPARE_StateAttribute_Parameter(_min_filter) + COMPARE_StateAttribute_Parameter(_mag_filter) + COMPARE_StateAttribute_Parameter(_shadow_compare_func) + COMPARE_StateAttribute_Parameter(_shadow_texture_mode) + COMPARE_StateAttribute_Parameter(_maxAnisotropy) + COMPARE_StateAttribute_Parameter(_minlod) + COMPARE_StateAttribute_Parameter(_maxlod) + COMPARE_StateAttribute_Parameter(_lodbias) + return 0; // passed all the above comparison macros, must be equal. +} +