From 399dac74781f08dc8f3f1f43c9b4e6464a19ebad Mon Sep 17 00:00:00 2001 From: Turky Mohammed <45469625+DelinWorks@users.noreply.github.com> Date: Sat, 6 Aug 2022 11:17:55 +0300 Subject: [PATCH] [PROPOSAL] Add wireframe rendering and function classification improvements. (#778) * Add wireframe rendering and function classification improvements. * Fix platform compilation. * Update CommandBufferGL.cpp GL_LINE and GL_FILL are no present in mobile devices, so the raw value has been used instead. * Update CommandBufferGL.cpp * Try fix IOS compilation * Update CommandBufferMTL.h [skip ci] * GLES & D3D11 wireframe Added crude but okay wireframe mode for GLES and D3D11 devices. --- core/3d/CCMesh.cpp | 12 ++++------ core/3d/CCMesh.h | 11 +++------ core/3d/CCMeshRenderer.cpp | 18 +++++--------- core/3d/CCMeshRenderer.h | 17 +++++++------ core/renderer/CCMaterial.cpp | 12 ++++++++++ core/renderer/CCMaterial.h | 24 +++++++++++++++++++ core/renderer/CCRenderCommand.h | 9 ++++++- core/renderer/CCRenderer.cpp | 5 ++-- core/renderer/backend/CommandBuffer.h | 8 +++++-- .../renderer/backend/metal/CommandBufferMTL.h | 9 +++++-- .../backend/metal/CommandBufferMTL.mm | 5 ++-- .../backend/opengl/CommandBufferGL.cpp | 22 ++++++++++++++--- .../renderer/backend/opengl/CommandBufferGL.h | 5 ++-- .../lua-bindings/auto/lua_axis_3d_auto.cpp | 20 +++++++++------- .../MeshRendererTest/MeshRendererTest.cpp | 2 +- 15 files changed, 122 insertions(+), 57 deletions(-) diff --git a/core/3d/CCMesh.cpp b/core/3d/CCMesh.cpp index c7924fa49c..f6a5202acf 100644 --- a/core/3d/CCMesh.cpp +++ b/core/3d/CCMesh.cpp @@ -111,8 +111,6 @@ static Texture2D* getDummyTexture() Mesh::Mesh() : _skin(nullptr) , _visible(true) - , _isTransparent(false) - , _force2DQueue(false) , meshIndexFormat(CustomCommand::IndexFormat::U_SHORT) , _meshIndexData(nullptr) , _blend(BlendFunc::ALPHA_NON_PREMULTIPLIED) @@ -390,12 +388,13 @@ void Mesh::draw(Renderer* renderer, uint32_t flags, unsigned int lightMask, const Vec4& color, - bool forceDepthWrite) + bool forceDepthWrite, + bool wireframe) { if (!isVisible()) return; - bool isTransparent = (_isTransparent || color.w < 1.f); + bool isTransparent = (_material->isTransparent() || color.w < 1.f); float globalZ = isTransparent ? 0 : globalZOrder; if (isTransparent) flags |= Node::FLAGS_RENDER_AS_3D; @@ -416,8 +415,6 @@ void Mesh::draw(Renderer* renderer, else _material->getStateBlock().setDepthWrite(true); - _material->getStateBlock().setBlend(_force2DQueue || isTransparent); - // set default uniforms for Mesh // 'u_color' and others const auto scene = Director::getInstance()->getRunningScene(); @@ -441,7 +438,8 @@ void Mesh::draw(Renderer* renderer, command.init(globalZ, transform); command.setSkipBatching(isTransparent); command.setTransparent(isTransparent); - command.set3D(!_force2DQueue); + command.set3D(!_material->isForce2DQueue()); + command.setWireframe(wireframe); } _meshIndexData->setPrimitiveType(_material->_drawPrimitive); diff --git a/core/3d/CCMesh.h b/core/3d/CCMesh.h index f412ff0e92..974b430a07 100644 --- a/core/3d/CCMesh.h +++ b/core/3d/CCMesh.h @@ -224,7 +224,8 @@ public: uint32_t flags, unsigned int lightMask, const Vec4& color, - bool forceDepthWrite); + bool forceDepthWrite, + bool wireframe); /**skin setter*/ void setSkin(MeshSkin* skin); @@ -239,11 +240,6 @@ public: */ void calculateAABB(); - /** - * force set this Mesh renderer to 2D render queue - */ - void setForce2DQueue(bool force2D) { _force2DQueue = force2D; } - std::string getTextureFileName() { return _texFile; } Mesh(); @@ -257,8 +253,7 @@ protected: std::map _textures; // textures that submesh is using MeshSkin* _skin; // skin bool _visible; // is the submesh visible - bool _isTransparent; // is this mesh transparent, it is a property of material in fact - bool _force2DQueue; // add this mesh to 2D render queue + CustomCommand::IndexFormat meshIndexFormat; std::string _name; diff --git a/core/3d/CCMeshRenderer.cpp b/core/3d/CCMeshRenderer.cpp index bf1f34bfa7..2027f00bfd 100644 --- a/core/3d/CCMeshRenderer.cpp +++ b/core/3d/CCMeshRenderer.cpp @@ -276,7 +276,9 @@ MeshRenderer::MeshRenderer() , _lightMask(-1) , _shaderUsingLight(false) , _forceDepthWrite(false) + , _wireframe(false) , _usingAutogeneratedGLProgram(true) + , _transparentMaterialHint(false) {} MeshRenderer::~MeshRenderer() @@ -408,7 +410,7 @@ MeshRenderer* MeshRenderer::createMeshRendererNode(NodeData* nodedata, ModelData texParams.sAddressMode = textureData->wrapS; texParams.tAddressMode = textureData->wrapT; tex->setTexParameters(texParams); - mesh->_isTransparent = (materialData->getTextureData(NTextureData::Usage::Transparency) != nullptr); + _transparentMaterialHint = materialData->getTextureData(NTextureData::Usage::Transparency) != nullptr; } } textureData = materialData->getTextureData(NTextureData::Usage::Normal); @@ -512,6 +514,7 @@ void MeshRenderer::genMaterial(bool useLight) for (auto&& mesh : _meshes) { auto material = materials[mesh->getMeshIndexData()->getMeshVertexData()]; + material->setTransparent(_transparentMaterialHint); // keep original state block if exist auto oldmaterial = mesh->getMaterial(); if (oldmaterial) @@ -573,8 +576,7 @@ void MeshRenderer::createNode(NodeData* nodedata, Node* root, const MaterialData texParams.sAddressMode = textureData->wrapS; texParams.tAddressMode = textureData->wrapT; tex->setTexParameters(texParams); - mesh->_isTransparent = - (materialData->getTextureData(NTextureData::Usage::Transparency) != nullptr); + _transparentMaterialHint = materialData->getTextureData(NTextureData::Usage::Transparency) != nullptr; } } textureData = materialData->getTextureData(NTextureData::Usage::Normal); @@ -811,7 +813,7 @@ void MeshRenderer::draw(Renderer* renderer, const Mat4& transform, uint32_t flag for (auto&& mesh : _meshes) { mesh->draw(renderer, _globalZOrder, transform, flags, _lightMask, Vec4(color.r, color.g, color.b, color.a), - _forceDepthWrite); + _forceDepthWrite, _wireframe); } } @@ -946,14 +948,6 @@ Mesh* MeshRenderer::getMesh() const return _meshes.at(0); } -void MeshRenderer::setForce2DQueue(bool force2D) -{ - for (const auto& mesh : _meshes) - { - mesh->setForce2DQueue(force2D); - } -} - /////////////////////////////////////////////////////////////////////////////////// MeshRendererCache* MeshRendererCache::_cacheInstance = nullptr; MeshRendererCache* MeshRendererCache::getInstance() diff --git a/core/3d/CCMeshRenderer.h b/core/3d/CCMeshRenderer.h index 0c9cc61c88..2e58a0d4fe 100644 --- a/core/3d/CCMeshRenderer.h +++ b/core/3d/CCMeshRenderer.h @@ -178,6 +178,11 @@ public: void setLightMask(unsigned int mask) { _lightMask = mask; } unsigned int getLightMask() const { return _lightMask; } + /** enables wireframe rendering mode for this mesh renderer only, this can be very useful for debugging and + understanding generated meshes. */ + void setWireframe(bool value) { _wireframe = value; } + bool isWireframe() const { return _wireframe; } + /** render all meshes within this mesh renderer */ virtual void draw(Renderer* renderer, const Mat4& transform, uint32_t flags) override; @@ -194,15 +199,11 @@ public: */ void setMaterial(Material* material, int meshIndex); - /** Adds a new material to a particular mesh in this mesh renderer. - * if meshIndex == -1, then it will be applied to all the meshes that belong to this mesh renderer. + /** Gets the material of a specific mesh in this mesh renderer. * - * @param meshIndex Index of the mesh to apply the material to. + * @param meshIndex Index of the mesh to get the material from. 0 is the default index. */ - Material* getMaterial(int meshIndex) const; - - /** force render this mesh renderer in 2D queue. */ - void setForce2DQueue(bool force2D); + Material* getMaterial(int meshIndex = 0) const; /** Get list of meshes used in this mesh renderer. */ const Vector& getMeshes() const { return _meshes; } @@ -265,7 +266,9 @@ protected: unsigned int _lightMask; bool _shaderUsingLight; // Is the current shader using lighting? bool _forceDepthWrite; // Always write to depth buffer + bool _wireframe; // render in wireframe mode bool _usingAutogeneratedGLProgram; + bool _transparentMaterialHint; // Generate transparent materials when building from files struct AsyncLoadParam { diff --git a/core/renderer/CCMaterial.cpp b/core/renderer/CCMaterial.cpp index 244dac24ad..6c443939a8 100644 --- a/core/renderer/CCMaterial.cpp +++ b/core/renderer/CCMaterial.cpp @@ -506,6 +506,18 @@ std::string Material::getName() const return _name; } +void Material::setTransparent(bool value) +{ + _isTransparent = value; + getStateBlock().setBlend(_force2DQueue || _isTransparent); +} + +void Material::setForce2DQueue(bool value) +{ + _force2DQueue = value; + getStateBlock().setBlend(_force2DQueue || _isTransparent); +} + Material::Material() : _name(""), _currentTechnique(nullptr), _target(nullptr) {} Material::~Material() {} diff --git a/core/renderer/CCMaterial.h b/core/renderer/CCMaterial.h index 31fba04b7d..8ec4c6d0ec 100644 --- a/core/renderer/CCMaterial.h +++ b/core/renderer/CCMaterial.h @@ -155,6 +155,27 @@ public: */ axis::backend::PrimitiveType getPrimitiveType() const { return _drawPrimitive; } + /** + * Enable material transparent rendering. + * WARNING: depth testing will not work. + */ + void setTransparent(bool value); + + /** + * Is material transparent? + */ + bool isTransparent() const { return _isTransparent; } + + /** + * Enable material 2D queue rendering. + */ + void setForce2DQueue(bool value); + + /** + * Is material in 2D render queue? + */ + bool isForce2DQueue() const { return _force2DQueue; } + protected: Material(); ~Material(); @@ -189,6 +210,9 @@ protected: std::unordered_map _textureSlots; int _textureSlotIndex = 0; + bool _isTransparent = false; // is this mesh transparent. + bool _force2DQueue = false; // render meshes using this material in 2D render queue. + axis::backend::PrimitiveType _drawPrimitive = axis::backend::PrimitiveType::TRIANGLE; // primitive draw type for meshes }; diff --git a/core/renderer/CCRenderCommand.h b/core/renderer/CCRenderCommand.h index be72676521..0142b27d2b 100644 --- a/core/renderer/CCRenderCommand.h +++ b/core/renderer/CCRenderCommand.h @@ -92,6 +92,10 @@ public: void set3D(bool value) { _is3D = value; } /**Get the depth by current model view matrix.*/ float getDepth() const { return _depth; } + /**Whether the command should be rendered in wireframe mode.*/ + bool isWireframe() const { return _isWireframe; } + /**Set wireframe render mode for this command.*/ + void setWireframe(bool value) { _isWireframe = value; } /// Can use the result to change the descriptor content. inline PipelineDescriptor& getPipelineDescriptor() { return _pipelineDescriptor; } @@ -123,9 +127,12 @@ protected: /** Is the command been rendered on 3D pass. */ bool _is3D = false; - /** Depth from the model view matrix.*/ + /** Depth from the model view matrix. */ float _depth = 0.f; + /** Polygon render mode set to LINE, which represents wireframe mode. */ + bool _isWireframe = false; + Mat4 _mv; PipelineDescriptor _pipelineDescriptor; diff --git a/core/renderer/CCRenderer.cpp b/core/renderer/CCRenderer.cpp index 9333965426..e35bdcb0c3 100644 --- a/core/renderer/CCRenderer.cpp +++ b/core/renderer/CCRenderer.cpp @@ -731,12 +731,13 @@ void Renderer::drawCustomCommand(RenderCommand* command) { _commandBuffer->setIndexBuffer(cmd->getIndexBuffer()); _commandBuffer->drawElements(cmd->getPrimitiveType(), cmd->getIndexFormat(), cmd->getIndexDrawCount(), - cmd->getIndexDrawOffset()); + cmd->getIndexDrawOffset(), cmd->isWireframe()); _drawnVertices += cmd->getIndexDrawCount(); } else { - _commandBuffer->drawArrays(cmd->getPrimitiveType(), cmd->getVertexDrawStart(), cmd->getVertexDrawCount()); + _commandBuffer->drawArrays(cmd->getPrimitiveType(), cmd->getVertexDrawStart(), cmd->getVertexDrawCount(), + cmd->isWireframe()); _drawnVertices += cmd->getVertexDrawCount(); } _drawnBatches++; diff --git a/core/renderer/backend/CommandBuffer.h b/core/renderer/backend/CommandBuffer.h index aba40bd294..c4c132f990 100644 --- a/core/renderer/backend/CommandBuffer.h +++ b/core/renderer/backend/CommandBuffer.h @@ -153,7 +153,10 @@ public: * @param count For each instance, the number of indexes to draw * @see `drawElements(PrimitiveType primitiveType, IndexFormat indexType, unsigned int count, unsigned int offset)` */ - virtual void drawArrays(PrimitiveType primitiveType, std::size_t start, std::size_t count) = 0; + virtual void drawArrays(PrimitiveType primitiveType, + std::size_t start, + std::size_t count, + bool wireframe = false) = 0; /** * Draw primitives with an index list. @@ -167,7 +170,8 @@ public: virtual void drawElements(PrimitiveType primitiveType, IndexFormat indexType, std::size_t count, - std::size_t offset) = 0; + std::size_t offset, + bool wireframe = false) = 0; /** * Do some resources release. diff --git a/core/renderer/backend/metal/CommandBufferMTL.h b/core/renderer/backend/metal/CommandBufferMTL.h index d5e90b4bf5..efc198a607 100644 --- a/core/renderer/backend/metal/CommandBufferMTL.h +++ b/core/renderer/backend/metal/CommandBufferMTL.h @@ -143,8 +143,10 @@ public: * @param start For each instance, the first index to draw * @param count For each instance, the number of indexes to draw * @see `drawElements(PrimitiveType primitiveType, IndexFormat indexType, unsigned int count, unsigned int offset)` + * + * TODO: Implement a wireframe mode for METAL devices. Refer to: https://forums.ogre3d.org/viewtopic.php?t=95089 */ - virtual void drawArrays(PrimitiveType primitiveType, std::size_t start, std::size_t count) override; + virtual void drawArrays(PrimitiveType primitiveType, std::size_t start, std::size_t count, bool wireframe) override; /** * Draw primitives with an index list. @@ -154,11 +156,14 @@ public: * @param offset Byte offset within indexBuffer to start reading indexes from. * @see `setIndexBuffer(Buffer* buffer)` * @see `drawArrays(PrimitiveType primitiveType, unsigned int start, unsigned int count)` + * + * TODO: Implement a wireframe mode for METAL devices. Refer to: https://forums.ogre3d.org/viewtopic.php?t=95089 */ virtual void drawElements(PrimitiveType primitiveType, IndexFormat indexType, std::size_t count, - std::size_t offset) override; + std::size_t offset, + bool wireframe) override; /** * Do some resources release. diff --git a/core/renderer/backend/metal/CommandBufferMTL.mm b/core/renderer/backend/metal/CommandBufferMTL.mm index 75b2efe37e..1096749d33 100644 --- a/core/renderer/backend/metal/CommandBufferMTL.mm +++ b/core/renderer/backend/metal/CommandBufferMTL.mm @@ -290,7 +290,7 @@ void CommandBufferMTL::setIndexBuffer(Buffer* buffer) [_mtlIndexBuffer retain]; } -void CommandBufferMTL::drawArrays(PrimitiveType primitiveType, std::size_t start, std::size_t count) +void CommandBufferMTL::drawArrays(PrimitiveType primitiveType, std::size_t start, std::size_t count, bool wireframe /* unused */) { prepareDrawing(); [_mtlRenderEncoder drawPrimitives:toMTLPrimitive(primitiveType) vertexStart:start vertexCount:count]; @@ -299,7 +299,8 @@ void CommandBufferMTL::drawArrays(PrimitiveType primitiveType, std::size_t start void CommandBufferMTL::drawElements(PrimitiveType primitiveType, IndexFormat indexType, std::size_t count, - std::size_t offset) + std::size_t offset, + bool wireframe /* unused */) { prepareDrawing(); [_mtlRenderEncoder drawIndexedPrimitives:toMTLPrimitive(primitiveType) diff --git a/core/renderer/backend/opengl/CommandBufferGL.cpp b/core/renderer/backend/opengl/CommandBufferGL.cpp index f945f3abb0..b1ee2a9b5e 100644 --- a/core/renderer/backend/opengl/CommandBufferGL.cpp +++ b/core/renderer/backend/opengl/CommandBufferGL.cpp @@ -211,24 +211,40 @@ void CommandBufferGL::setProgramState(ProgramState* programState) _programState = programState; } -void CommandBufferGL::drawArrays(PrimitiveType primitiveType, std::size_t start, std::size_t count) +void CommandBufferGL::drawArrays(PrimitiveType primitiveType, std::size_t start, std::size_t count, bool wireframe) { prepareDrawing(); +#ifdef AX_USE_GL // glPolygonMode is only supported in Desktop OpenGL + if (wireframe) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); +#else + if (wireframe) primitiveType = PrimitiveType::LINE; +#endif glDrawArrays(UtilsGL::toGLPrimitiveType(primitiveType), start, count); - +#ifdef AX_USE_GL // glPolygonMode is only supported in Desktop OpenGL + if (wireframe) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); +#endif cleanResources(); } void CommandBufferGL::drawElements(PrimitiveType primitiveType, IndexFormat indexType, std::size_t count, - std::size_t offset) + std::size_t offset, + bool wireframe) { prepareDrawing(); +#ifdef AX_USE_GL // glPolygonMode is only supported in Desktop OpenGL + if (wireframe) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); +#else + if (wireframe) primitiveType = PrimitiveType::LINE; +#endif glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer->getHandler()); glDrawElements(UtilsGL::toGLPrimitiveType(primitiveType), count, UtilsGL::toGLIndexType(indexType), (GLvoid*)offset); CHECK_GL_ERROR_DEBUG(); +#ifdef AX_USE_GL // glPolygonMode is only supported in Desktop OpenGL + if (wireframe) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); +#endif cleanResources(); } diff --git a/core/renderer/backend/opengl/CommandBufferGL.h b/core/renderer/backend/opengl/CommandBufferGL.h index 95c68fb409..8a99075c2a 100644 --- a/core/renderer/backend/opengl/CommandBufferGL.h +++ b/core/renderer/backend/opengl/CommandBufferGL.h @@ -139,7 +139,7 @@ public: * @param count For each instance, the number of indexes to draw * @see `drawElements(PrimitiveType primitiveType, IndexFormat indexType, unsigned int count, unsigned int offset)` */ - virtual void drawArrays(PrimitiveType primitiveType, std::size_t start, std::size_t count) override; + virtual void drawArrays(PrimitiveType primitiveType, std::size_t start, std::size_t count, bool wireframe = false) override; /** * Draw primitives with an index list. @@ -153,7 +153,8 @@ public: virtual void drawElements(PrimitiveType primitiveType, IndexFormat indexType, std::size_t count, - std::size_t offset) override; + std::size_t offset, + bool wireframe = false) override; /** * Do some resources release. diff --git a/extensions/scripting/lua-bindings/auto/lua_axis_3d_auto.cpp b/extensions/scripting/lua-bindings/auto/lua_axis_3d_auto.cpp index 6bbb05dc1c..bd72406880 100644 --- a/extensions/scripting/lua-bindings/auto/lua_axis_3d_auto.cpp +++ b/extensions/scripting/lua-bindings/auto/lua_axis_3d_auto.cpp @@ -2258,7 +2258,7 @@ int lua_axis_3d_Mesh_draw(lua_State* tolua_S) #endif argc = lua_gettop(tolua_S)-1; - if (argc == 7) + if (argc == 8) { axis::Renderer* arg0; double arg1; @@ -2267,26 +2267,30 @@ int lua_axis_3d_Mesh_draw(lua_State* tolua_S) unsigned int arg4; axis::Vec4 arg5; bool arg6; + bool arg7; ok &= luaval_to_object(tolua_S, 2, "ax.Renderer",&arg0, "ax.Mesh:draw"); - ok &= luaval_to_number(tolua_S, 3,&arg1, "ax.Mesh:draw"); + ok &= luaval_to_number(tolua_S, 3, &arg1, "ax.Mesh:draw"); ok &= luaval_to_mat4(tolua_S, 4, &arg2, "ax.Mesh:draw"); - ok &= luaval_to_uint32(tolua_S, 5,&arg3, "ax.Mesh:draw"); + ok &= luaval_to_uint32(tolua_S, 5, &arg3, "ax.Mesh:draw"); - ok &= luaval_to_uint32(tolua_S, 6,&arg4, "ax.Mesh:draw"); + ok &= luaval_to_uint32(tolua_S, 6, &arg4, "ax.Mesh:draw"); ok &= luaval_to_vec4(tolua_S, 7, &arg5, "ax.Mesh:draw"); - ok &= luaval_to_boolean(tolua_S, 8,&arg6, "ax.Mesh:draw"); + ok &= luaval_to_boolean(tolua_S, 8, &arg6, "ax.Mesh:draw"); + + ok &= luaval_to_boolean(tolua_S, 9, &arg7, "ax.Mesh:draw"); + if(!ok) { tolua_error(tolua_S,"invalid arguments in function 'lua_axis_3d_Mesh_draw'", nullptr); return 0; } - cobj->draw(arg0, arg1, arg2, arg3, arg4, arg5, arg6); + cobj->draw(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7); lua_settop(tolua_S, 1); return 1; } @@ -2627,7 +2631,7 @@ int lua_axis_3d_Mesh_setForce2DQueue(lua_State* tolua_S) tolua_error(tolua_S,"invalid arguments in function 'lua_axis_3d_Mesh_setForce2DQueue'", nullptr); return 0; } - cobj->setForce2DQueue(arg0); + cobj->getMaterial()->setForce2DQueue(arg0); lua_settop(tolua_S, 1); return 1; } @@ -4480,7 +4484,7 @@ int lua_axis_3d_MeshRenderer_setForce2DQueue(lua_State* tolua_S) tolua_error(tolua_S,"invalid arguments in function 'lua_axis_3d_MeshRenderer_setForce2DQueue'", nullptr); return 0; } - cobj->setForce2DQueue(arg0); + cobj->getMaterial()->setForce2DQueue(arg0); lua_settop(tolua_S, 1); return 1; } diff --git a/tests/cpp-tests/Classes/MeshRendererTest/MeshRendererTest.cpp b/tests/cpp-tests/Classes/MeshRendererTest/MeshRendererTest.cpp index abed285b3e..d94e2cdf34 100644 --- a/tests/cpp-tests/Classes/MeshRendererTest/MeshRendererTest.cpp +++ b/tests/cpp-tests/Classes/MeshRendererTest/MeshRendererTest.cpp @@ -2214,7 +2214,7 @@ MeshRendererClippingTest::MeshRendererClippingTest() auto animation = Animation3D::create("MeshRendererTest/orc.c3b"); auto animate = Animate3D::create(animation); mesh3D->runAction(RepeatForever::create(animate)); - mesh3D->setForce2DQueue(true); + mesh3D->getMaterial()->setForce2DQueue(true); } MeshRendererClippingTest::~MeshRendererClippingTest() {}