Commit 2ec9c2eb authored by Robert Menzel's avatar Robert Menzel

Reworking Textures

* Sampler have getters for all parameters
* Texture gets a base class
* TextureBuffer moved to own file

Texture is still compatible with the old Texture class, new classes will be based on TextureBase and Texture will only remain for compatibility!
parent 7b40da95
...@@ -71,17 +71,14 @@ public: ...@@ -71,17 +71,14 @@ public:
} }
inline void setMinFilter(GLenum _value = GL_NEAREST_MIPMAP_LINEAR) { glSamplerParameteri( mObjectName, GL_TEXTURE_MIN_FILTER, _value); } inline void setMinFilter(GLenum _value = GL_NEAREST_MIPMAP_LINEAR) { glSamplerParameteri( mObjectName, GL_TEXTURE_MIN_FILTER, _value); }
inline void setMagFilter(GLenum _value = GL_LINEAR) { glSamplerParameteri( mObjectName, GL_TEXTURE_MAG_FILTER, _value); } inline void setMagFilter(GLenum _value = GL_LINEAR) { glSamplerParameteri( mObjectName, GL_TEXTURE_MAG_FILTER, _value); }
inline void setWrapS( GLenum _wrapS = GL_REPEAT ) { glSamplerParameteri( mObjectName, GL_TEXTURE_WRAP_S, _wrapS); }
void setWrap( GLenum _wrapS );
void setWrap( GLenum _wrapS, GLenum _wrapT );
void setWrap( GLenum _wrapS, GLenum _wrapT, GLenum _wrapR );
inline void setWrapS( GLenum _wrapS = GL_REPEAT ) { glSamplerParameteri( mObjectName, GL_TEXTURE_WRAP_S, _wrapS); }
inline void setWrapT( GLenum _wrapT = GL_REPEAT ) { glSamplerParameteri( mObjectName, GL_TEXTURE_WRAP_T, _wrapT); } inline void setWrapT( GLenum _wrapT = GL_REPEAT ) { glSamplerParameteri( mObjectName, GL_TEXTURE_WRAP_T, _wrapT); }
inline void setWrapR( GLenum _wrapR = GL_REPEAT ) { glSamplerParameteri( mObjectName, GL_TEXTURE_WRAP_R, _wrapR); } inline void setWrapR( GLenum _wrapR = GL_REPEAT ) { glSamplerParameteri( mObjectName, GL_TEXTURE_WRAP_R, _wrapR); }
void setWrap(GLenum _wrapS, GLenum _wrapT = 0, GLenum _wrapR = 0)
{
setWrapS(_wrapS);
if (_wrapT != 0) setWrapT(_wrapT);
if (_wrapR != 0) setWrapR(_wrapR);
}
inline void setMinLOD( GLint _lod = -1000 ) { glSamplerParameteri( mObjectName, GL_TEXTURE_MIN_LOD, _lod); } inline void setMinLOD( GLint _lod = -1000 ) { glSamplerParameteri( mObjectName, GL_TEXTURE_MIN_LOD, _lod); }
inline void setMaxLOD( GLint _lod = 1000 ) { glSamplerParameteri( mObjectName, GL_TEXTURE_MAX_LOD, _lod); } inline void setMaxLOD( GLint _lod = 1000 ) { glSamplerParameteri( mObjectName, GL_TEXTURE_MAX_LOD, _lod); }
inline void setLODBias( GLfloat _bias = 0.0f ) { glSamplerParameterf( mObjectName, GL_TEXTURE_LOD_BIAS, _bias); } inline void setLODBias( GLfloat _bias = 0.0f ) { glSamplerParameterf( mObjectName, GL_TEXTURE_LOD_BIAS, _bias); }
...@@ -91,6 +88,26 @@ public: ...@@ -91,6 +88,26 @@ public:
//! _sampleCount = 1.0 to deactivate anisotrop filtering, maximum is often 16. If a value is too high it will get clamped to the maximum //! _sampleCount = 1.0 to deactivate anisotrop filtering, maximum is often 16. If a value is too high it will get clamped to the maximum
void setMaxAnisotropy( GLfloat _sampleCount = 1.0f ); void setMaxAnisotropy( GLfloat _sampleCount = 1.0f );
///////////////////////////////////////////////////////////////////////////////////////
// getter
///////////////////////////////////////////////////////////////////////////////////////
//! generic getter
GLint getParameterI( GLenum _parameterName );
GLfloat getParameterF( GLenum _parameterName );
GLenum getMinFilter() { return (GLenum) getParameterI( GL_TEXTURE_MIN_FILTER ); }
GLenum getMagFilter() { return (GLenum) getParameterI( GL_TEXTURE_MAG_FILTER ); }
GLenum getWrapS() { return (GLenum) getParameterI( GL_TEXTURE_WRAP_S ); }
GLenum getWrapT() { return (GLenum) getParameterI( GL_TEXTURE_WRAP_T ); }
GLenum getWrapR() { return (GLenum) getParameterI( GL_TEXTURE_WRAP_R ); }
GLfloat getMinLOD() { return getParameterF( GL_TEXTURE_MIN_LOD ); }
GLfloat getMaxLOD() { return getParameterF( GL_TEXTURE_MAX_LOD ); }
GLfloat getLODBias() { return getParameterF( GL_TEXTURE_LOD_BIAS ); }
GLint getCompareMode() { return getParameterI( GL_TEXTURE_COMPARE_MODE ); }
GLint getCompareFunc() { return getParameterI( GL_TEXTURE_COMPARE_FUNC ); }
glm::vec4 getBorderColor();
private: private:
GLuint mObjectName; GLuint mObjectName;
}; };
......
...@@ -22,153 +22,28 @@ ...@@ -22,153 +22,28 @@
#include <vector> #include <vector>
#include <ACGL/OpenGL/Objects/Buffer.hh>
namespace ACGL{ namespace ACGL{
namespace OpenGL{ namespace OpenGL{
/*
class TextureNG
{
ACGL_NOT_COPYABLE(TextureNG)
public:
TextureNG(GLenum _target) : mObjectName(0),
mTarget(_target) {
glGenTextures(1, &mObjectName);
if (openGLCriticalErrorOccured() ) {
ACGL::Utils::error() << "could not generate texture!" << std::endl;
}
}
virtual ~TextureNG()
{
// object name 0 will get ignored by OpenGL
glDeleteTextures(1, &mObjectName);
}
GLuint getObjectName () const { return mObjectName; }
GLenum getTarget () const { return mTarget; }
GLsizei getWidth () const;
GLsizei getHeight () const;
GLsizei getDepth () const;
glm::uvec3 getSize () const { return glm::uvec3( getWidth(), getHeight(), getDepth() ); }
GLenum getInternalFormat () const;
GLenum getFormat () const;
GLenum getType () const;
GLint getMinFilter () const;
GLint getMagFilter () const;
#ifndef ACGL_OPENGLES_VERSION_20
GLenum getWrapS () const;
GLenum getWrapT () const;
GLenum getWrapR () const;
#endif // ACGL_OPENGLES_VERSION_20
//! Activate texture unit and bind this texture.
void bind(GLuint _textureUnit = 0) const
{
glActiveTexture(GL_TEXTURE0 + _textureUnit);
glBindTexture(mTarget, mObjectName);
openGLRareError();
}
private:
GLuint mObjectName;
GLenum mTarget;
};
ACGL_SMARTPOINTER_TYPEDEFS(TextureNG)
*/
#if (ACGL_OPENGL_VERSION >= 30)
/* /*
* TextureBuffers are 1D textures which store there data in a Buffer. * A Texture consists of:
* They are useful to access large chunks of data from within a shader. * 1. 1..N Images (e.g. mipmap layers)
* Technically they are accessed as textures (uniform samplerBuffer) via texelFetch * 2. an internal data format (datatype and number of channels on the GPU)
* with a 1D coordinate (int coordinate, so no filtering). This means they benefit from * 3. a sampling mode (alternatively a sampler object can be used)
* texture caches. *
* Use these if the data doesn't fit into a UniformBufferObject. * The TextureBase class holds the methods to define 2 & 3, the Images (incl. getter and setter)
* vary between different texture types, so subclasses will take care if that.
* *
* Data gets stored in the Buffer, no glTexImage calles are allowed! * Note that changing the sampler mode might bind this texture to the currently active texture unit
* (but don't rely on it as this 'side effect' might get removed later on).
*
* Multiple states, e.g. the sampling mode gets stored in a texture as getting this infos back from
* GL is not trivial (at least if the currently bound texture should not change).
*/ */
class TextureBuffer : public Buffer class TextureBase
{
public:
// create a new BufferObject with _reservedMemory space (in bytes!)
TextureBuffer( GLenum _dataType, size_t _reservedMemory = 1 ) : Buffer(GL_TEXTURE_BUFFER) {
glGenTextures(1, &mTextureObjectName);
if (openGLCriticalErrorOccured() ) {
ACGL::Utils::error() << "could not generate texture object for texture buffer!" << std::endl;
}
mDataType = _dataType;
Buffer::setData( _reservedMemory );
attachBufferToTexture();
}
// use an existing BufferObject
TextureBuffer( GLenum _dataType, SharedBufferObject _pBuffer ) : Buffer(_pBuffer, GL_TEXTURE_BUFFER) {
glGenTextures(1, &mTextureObjectName);
if (openGLCriticalErrorOccured() ) {
ACGL::Utils::error() << "could not generate texture object for texture buffer!" << std::endl;
}
mDataType = _dataType;
attachBufferToTexture();
}
~TextureBuffer() {
setBufferObject( SharedBufferObject() ); // detach the Buffer
glDeleteTextures(1, &mTextureObjectName);
}
//! the GL buffer can get changed at any time
void setBufferObject( SharedBufferObject _pBuffer ) {
Buffer::setBufferObject( _pBuffer );
if (_pBuffer == NULL) {
// detach all buffers:
glTexBuffer( GL_TEXTURE_BUFFER, mDataType, 0 );
} else {
attachBufferToTexture();
}
}
//! Bind the texture part to access it from a shader
void bindTexture(GLuint _textureUnit = 0) const {
glActiveTexture(GL_TEXTURE0 + _textureUnit);
glBindTexture(GL_TEXTURE_BUFFER, mTextureObjectName);
openGLRareError();
}
//! Bind the buffer part to change the data
void bindBuffer() const {
Buffer::bind();
}
inline GLuint getTextureObjectName() const { return mTextureObjectName; }
inline GLuint getBufferObjectName() const { return Buffer::getObjectName(); }
private:
//! private to prevent it from being called -> it's not clear whether the texture or the buffer should get bound, call
//! bindBuffer() or bindTexture() directly!
void bind() {}
void attachBufferToTexture() {
assert( Buffer::getSize() > 0 && "glTexBuffer will fail if the buffer is empty" );
bindTexture();
openGLCriticalErrorOccured();
glTexBuffer( GL_TEXTURE_BUFFER, mDataType, Buffer::getObjectName() );
openGLCriticalErrorOccured();
}
GLenum mDataType;
GLuint mTextureObjectName;
};
ACGL_SMARTPOINTER_TYPEDEFS(TextureBuffer)
#endif // OpenGL 3.0+
class Texture
{ {
ACGL_NOT_COPYABLE(Texture) ACGL_NOT_COPYABLE(TextureBase)
// ========================================================================================================= \/ // ========================================================================================================= \/
// ============================================================================================ CONSTRUCTORS \/ // ============================================================================================ CONSTRUCTORS \/
...@@ -178,14 +53,13 @@ public: ...@@ -178,14 +53,13 @@ public:
/*! /*!
Default texture parameters taken from: http://www.opengl.org/sdk/docs/man/xhtml/glTexParameter.xml Default texture parameters taken from: http://www.opengl.org/sdk/docs/man/xhtml/glTexParameter.xml
*/ */
Texture(GLenum _target) TextureBase(GLenum _target)
: mObjectName(0), : mObjectName(0),
mTarget(_target), mTarget(_target),
mWidth(0), mWidth(0),
mHeight(1), mHeight(1),
mDepth(1), mDepth(1),
mInternalFormat(GL_RGBA), mInternalFormat(GL_RGBA),
mFormat(GL_RGBA),
mType(GL_UNSIGNED_BYTE), mType(GL_UNSIGNED_BYTE),
mMinFilter(GL_NEAREST_MIPMAP_LINEAR), mMinFilter(GL_NEAREST_MIPMAP_LINEAR),
mMagFilter(GL_LINEAR), mMagFilter(GL_LINEAR),
...@@ -199,7 +73,7 @@ public: ...@@ -199,7 +73,7 @@ public:
} }
} }
virtual ~Texture(void) virtual ~TextureBase(void)
{ {
// object name 0 will get ignored by OpenGL // object name 0 will get ignored by OpenGL
glDeleteTextures(1, &mObjectName); glDeleteTextures(1, &mObjectName);
...@@ -215,39 +89,38 @@ public: ...@@ -215,39 +89,38 @@ public:
inline GLsizei getHeight (void) const { return mHeight; } inline GLsizei getHeight (void) const { return mHeight; }
inline GLsizei getDepth (void) const { return mDepth; } inline GLsizei getDepth (void) const { return mDepth; }
inline GLenum getInternalFormat (void) const { return mInternalFormat; } inline GLenum getInternalFormat (void) const { return mInternalFormat; }
inline GLenum getFormat (void) const { return mFormat; }
inline GLenum getType (void) const { return mType; } inline GLenum getType (void) const { return mType; }
inline GLint getMinFilter (void) const { return mMinFilter; } inline GLint getMinFilter (void) const { return mMinFilter; }
inline GLint getMagFilter (void) const { return mMagFilter; } inline GLint getMagFilter (void) const { return mMagFilter; }
#ifndef ACGL_OPENGLES_VERSION_20 #ifndef ACGL_OPENGLES_VERSION_20
inline GLenum getWrapS (void) const { return mWrapS; } inline GLenum getWrapS (void) const { return mWrapS; }
inline GLenum getWrapT (void) const { return mWrapT; } inline GLenum getWrapT (void) const { return mWrapT; }
inline GLenum getWrapR (void) const { return mWrapR; } inline GLenum getWrapR (void) const { return mWrapR; }
#endif // ACGL_OPENGLES_VERSION_20 #endif // ACGL_OPENGLES_VERSION_20
inline glm::uvec3 getSize (void) const { return glm::uvec3( mWidth, mHeight, mDepth ); } inline glm::uvec3 getSize (void) const { return glm::uvec3( mWidth, mHeight, mDepth ); }
// ===================================================================================================== \/ // ===================================================================================================== \/
// ============================================================================================ WRAPPERS \/ // ============================================================================================ WRAPPERS \/
// ===================================================================================================== \/ // ===================================================================================================== \/
public: public:
//! Activate texture unit and bind this texture. //! Activate texture unit and bind this texture to it. _textureUnit starts at 0!
inline void bind(GLuint _textureUnit) const inline void bind(GLuint _textureUnit) const
{ {
glActiveTexture(GL_TEXTURE0 + _textureUnit); glActiveTexture(GL_TEXTURE0 + _textureUnit);
glBindTexture(mTarget, mObjectName); glBindTexture(mTarget, mObjectName);
openGLRareError(); openGLRareError();
} }
//! Bind this texture. //! Bind this texture to the currently active texture unit.
inline void bind(void) const inline void bind(void) const
{ {
glBindTexture(mTarget, mObjectName); glBindTexture(mTarget, mObjectName);
openGLRareError(); openGLRareError();
} }
//! Note: The function is not const, because it changes the corresponding GPU data //! Note, side-effect: The function will bind this texture!
inline void setMinFilter(GLint _value) inline void setMinFilter(GLint _value)
{ {
mMinFilter = _value; mMinFilter = _value;
...@@ -256,7 +129,7 @@ public: ...@@ -256,7 +129,7 @@ public:
openGLRareError(); openGLRareError();
} }
//! Note: The function is not const, because it changes the corresponding GPU data //! Note, side-effect: The function will bind this texture!
inline void setMagFilter(GLint _value) inline void setMagFilter(GLint _value)
{ {
mMagFilter = _value; mMagFilter = _value;
...@@ -265,16 +138,70 @@ public: ...@@ -265,16 +138,70 @@ public:
openGLRareError(); openGLRareError();
} }
void setWrapS( GLenum _wrapS = GL_REPEAT ); #ifndef ACGL_OPENGLES_VERSION_20
void setWrapT( GLenum _wrapT = GL_REPEAT ); void setWrapS( GLenum _wrapS = GL_REPEAT );
void setWrapR( GLenum _wrapR = GL_REPEAT ); void setWrapT( GLenum _wrapT = GL_REPEAT );
void setWrapR( GLenum _wrapR = GL_REPEAT );
//! Note: The function is not const, because it changes the corresponding GPU data //! Note: The function will bind this texture!
void setWrap(GLenum _wrapS, GLenum _wrapT = 0, GLenum _wrapR = 0); void setWrap(GLenum _wrapS, GLenum _wrapT = 0, GLenum _wrapR = 0);
#endif
//! _sampleCount = 1.0 to deactivate anisotrop filtering, maximum is often 16. If a value is too high it will get clamped to the maximum //! _sampleCount = 1.0 to deactivate anisotrop filtering, maximum is often 16. If a value is too high it will get clamped to the maximum
void setAnisotropicFilter( GLfloat _sampleCount ); void setAnisotropicFilter( GLfloat _sampleCount );
//! Generate mipmaps from the current base texture (i.e. the texture from level 0)
//! Note: The function is not const, because it changes the corresponding GPU data
void generateMipmaps(void);
// =================================================================================================== \/
// ============================================================================================ FIELDS \/
// =================================================================================================== \/
protected:
GLuint mObjectName;
// kind of texture data:
GLenum mTarget;
GLsizei mWidth;
GLsizei mHeight;
GLsizei mDepth;
GLenum mInternalFormat;
GLenum mType;
// sampling parameters:
GLint mMinFilter;
GLint mMagFilter;
GLenum mWrapS;
GLenum mWrapT;
GLenum mWrapR;
};
ACGL_SMARTPOINTER_TYPEDEFS(TextureBase)
//
// Full compatibility with the old Texture class:
//
class Texture : public TextureBase
{
//ACGL_NOT_COPYABLE(Texture)
// ========================================================================================================= \/
// ============================================================================================ CONSTRUCTORS \/
// ========================================================================================================= \/
public:
Texture(GLenum _target) : TextureBase(_target) { mFormat = GL_RGBA; }
// ==================================================================================================== \/
// ============================================================================================ GETTERS \/
// ==================================================================================================== \/
public:
inline GLenum getFormat (void) const { return mFormat; }
// ===================================================================================================== \/
// ============================================================================================ WRAPPERS \/
// ===================================================================================================== \/
public:
//! Set texture data for 1D, 2D or 3D textures //! Set texture data for 1D, 2D or 3D textures
inline void setImageData(const GLvoid* _pData = NULL) inline void setImageData(const GLvoid* _pData = NULL)
{ {
...@@ -293,6 +220,7 @@ public: ...@@ -293,6 +220,7 @@ public:
} }
#endif #endif
} }
//! Set texture data for 1D, 2D or 3D textures //! Set texture data for 1D, 2D or 3D textures
inline void setImageData( inline void setImageData(
const GLvoid* _pData, const GLvoid* _pData,
...@@ -482,27 +410,6 @@ public: ...@@ -482,27 +410,6 @@ public:
//! Returns a TextureData objects holding the contents of the texture //! Returns a TextureData objects holding the contents of the texture
SharedTextureData getImageData(GLint _lod = 0, GLenum _type = GL_INVALID_ENUM) const; SharedTextureData getImageData(GLint _lod = 0, GLenum _type = GL_INVALID_ENUM) const;
#endif #endif
//! Generate mipmaps from the current base texture (i.e. the texture from level 0)
//! Note: The function is not const, because it changes the corresponding GPU data
void generateMipmaps(void)
{
glBindTexture(mTarget, mObjectName);
#if (!defined ACGL_OPENGL_PROFILE_CORE)
// on some ATI systems texturing has to be enabled to generate MipMaps
// this is not needed by the spec an deprecated on core profiles (generates
// an error on MacOS X Lion)
glEnable(mTarget);
openGLRareError();
#endif
#ifdef ACGL_OPENGL_VERSION_21
// OpenGL 2 way to generate MipMaps
glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
#else
glGenerateMipmap(mTarget);
#endif
openGLRareError();
}
private: private:
//! Set texture data //! Set texture data
...@@ -551,19 +458,7 @@ private: ...@@ -551,19 +458,7 @@ private:
// ============================================================================================ FIELDS \/ // ============================================================================================ FIELDS \/
// =================================================================================================== \/ // =================================================================================================== \/
private: private:
GLuint mObjectName;
GLenum mTarget;
GLsizei mWidth;
GLsizei mHeight;
GLsizei mDepth;
GLenum mInternalFormat;
GLenum mFormat; GLenum mFormat;
GLenum mType;
GLint mMinFilter;
GLint mMagFilter;
GLenum mWrapS;
GLenum mWrapT;
GLenum mWrapR;
}; };
ACGL_SMARTPOINTER_TYPEDEFS(Texture) ACGL_SMARTPOINTER_TYPEDEFS(Texture)
......
/***********************************************************************
* Copyright 2011-2012 Computer Graphics Group RWTH Aachen University. *
* All rights reserved. *
* Distributed under the terms of the MIT License (see LICENSE.TXT). *
**********************************************************************/
#ifndef ACGL_OPENGL_OBJECTS_TEXTUREBUFFER_HH
#define ACGL_OPENGL_OBJECTS_TEXTUREBUFFER_HH
/**
* A Texture wrapps the OpenGL texture buffer.
*
* TextureBuffers are 1D textures which store there data in a Buffer.
* They are useful to access large chunks of data from within a shader.
* Technically they are accessed as textures (uniform samplerBuffer) via texelFetch
* with a 1D coordinate (int coordinate, so no filtering). This means they benefit from
* texture caches.
* Use these if the data doesn't fit into a UniformBufferObject.
*
* Data gets stored in the Buffer, no glTexImage calles are allowed!
*/
#include <ACGL/ACGL.hh>
#include <ACGL/Base/Macros.hh>
#include <ACGL/OpenGL/GL.hh>
#include <ACGL/OpenGL/Tools.hh>
#include <ACGL/OpenGL/Objects/Buffer.hh>
#if (ACGL_OPENGL_VERSION >= 30)
namespace ACGL{
namespace OpenGL{
class TextureBuffer : public Buffer
{
public:
// create a new BufferObject with _reservedMemory space (in bytes!)
TextureBuffer( GLenum _dataType, size_t _reservedMemory = 1 ) : Buffer(GL_TEXTURE_BUFFER) {
glGenTextures(1, &mTextureObjectName);
if (openGLCriticalErrorOccured() ) {
ACGL::Utils::error() << "could not generate texture object for texture buffer!" << std::endl;
}
mDataType = _dataType;
Buffer::setData( _reservedMemory );
attachBufferToTexture();
}
// use an existing BufferObject
TextureBuffer( GLenum _dataType, SharedBufferObject _pBuffer ) : Buffer(_pBuffer, GL_TEXTURE_BUFFER) {
glGenTextures(1, &mTextureObjectName);
if (openGLCriticalErrorOccured() ) {
ACGL::Utils::error() << "could not generate texture object for texture buffer!" << std::endl;
}
mDataType = _dataType;
attachBufferToTexture();
}
~TextureBuffer() {
setBufferObject( SharedBufferObject() ); // detach the Buffer
glDeleteTextures(1, &mTextureObjectName);
}
//! the GL buffer can get changed at any time
void setBufferObject( SharedBufferObject _pBuffer ) {
Buffer::setBufferObject( _pBuffer );
if (_pBuffer == NULL) {
// detach all buffers:
glTexBuffer( GL_TEXTURE_BUFFER, mDataType, 0 );
} else {
attachBufferToTexture();
}
}
//! Bind the texture part to access it from a shader
void bindTexture(GLuint _textureUnit = 0) const {
glActiveTexture(GL_TEXTURE0 + _textureUnit);
glBindTexture(GL_TEXTURE_BUFFER, mTextureObjectName);
openGLRareError();
}
//! Bind the buffer part to change the data
void bindBuffer() const {
Buffer::bind();
}
inline GLuint getTextureObjectName() const { return mTextureObjectName; }
inline GLuint getBufferObjectName() const { return Buffer::getObjectName(); }
private:
//! private to prevent it from being called -> it's not clear whether the texture or the buffer should get bound, call
//! bindBuffer() or bindTexture() directly!
void bind() {}
void attachBufferToTexture() {
assert( Buffer::getSize() > 0 && "glTexBuffer will fail if the buffer is empty" );
bindTexture();
openGLCriticalErrorOccured();
glTexBuffer( GL_TEXTURE_BUFFER, mDataType, Buffer::getObjectName() );
openGLCriticalErrorOccured();
}