void ShaderShadowMapEngine::calcPointLightMatrices( Matrixr &matWorldToLight, Matrixr &matEyeToLight, const PointLight *pointL, const Matrixr &matEyeToWorld) { if(pointL->getBeacon() != NULL) pointL->getBeacon()->getToWorld(matWorldToLight); Matrixr matLightPos; matLightPos .setTranslate(pointL->getPosition()); matWorldToLight.mult (matLightPos ); matWorldToLight.invert ( ); matEyeToLight = matWorldToLight; matEyeToLight.mult(matEyeToWorld); }
void Node::getWorldVolume(BoxVolume &result) { Matrixr m; if(getParent() != NULL) { getParent()->getToWorld(m); } else { m.setIdentity(); } updateVolume(); result = getVolume(); result.transform(m); }
/*! Calculates \a matWorldToLight and \a matEyeToLight for a directional light \a dirL and inverse viewing matrix \a matEyeToWorld. */ void ShaderShadowMapEngine::calcDirectionalLightMatrices( Matrixr &matWorldToLight, Matrixr &matEyeToLight, const DirectionalLight *dirL, const Matrixr &matEyeToWorld) { if(dirL->getBeacon() != NULL) dirL->getBeacon()->getToWorld(matWorldToLight); Quaternion rotLightDir (Vec3r(0.f, 0.f, 1.f), dirL->getDirection()); Matrixr matLightDir; matLightDir.setRotate(rotLightDir); matWorldToLight.mult (matLightDir); matWorldToLight.invert( ); matEyeToLight = matWorldToLight; matEyeToLight.mult(matEyeToWorld); }
void Node::getToWorld(Matrixr &result) { if(getParent() != NULL) { getParent()->getToWorld(result); } else { result.setIdentity(); } if(getCore() != NULL) getCore()->accumulateMatrix(result); }
/*! Calculates \a matWorldToLight and \a matEyeToLight for a spot light \a spotL and inverse viewing matrix \a matEyeToWorld. */ void ShaderShadowMapEngine::calcSpotLightMatrices( Matrixr &matWorldToLight, Matrixr &matEyeToLight, const SpotLight *spotL, const Matrixr &matEyeToWorld) { if(spotL->getBeacon() != NULL) spotL->getBeacon()->getToWorld(matWorldToLight); Matrixr matLightPos; matLightPos.setTranslate(spotL->getPosition()); Matrixr matLightDir; Quaternion rotLightDir(Vec3r(0.f, 0.f, 1.f), -spotL->getDirection()); matLightDir.setRotate(rotLightDir); matWorldToLight.mult (matLightPos); matWorldToLight.mult (matLightDir); matWorldToLight.invert( ); matEyeToLight = matWorldToLight; matEyeToLight.mult(matEyeToWorld); }
void Joint::accumulateMatrix(Matrixr &result) { Inherited::accumulateMatrix(result); result.mult(getJointTransformation()); }
/*! Calculates the trapezoidal transformation matrix \a matNT that transforms post projection light space so that shadow map resolution in the "foreground" is maximized. The major steps are: - compute the intersection of eyeFrust and lightFrust - construct a trapezoid that contains the intersection - determine the transformation that maps this trapezoid to the (-1, 1) square Returns \c true if the transform was computed, \c false otherwise (e.g. if the intersection of eyeFrust and lightFrust is empty). For details see "T. Martin, T.-S. Tan: Anti-aliasing and Continuity with Trapezoidal Shadow Maps" */ bool TrapezoidalShadowMapEngine::calcTrapezoidalTransform( Matrixr &matNT, const Matrixr &matEyeToWorld, const Matrixr &matLightFull, const FrustumVolume &eyeFrust, const FrustumVolume &lightFrust ) { // obtain post proj. light space eye position Pnt3r eyePos; matEyeToWorld.mult (eyePos, eyePos); matLightFull .multFull(eyePos, eyePos); // intersect eye and light frusta, get vertices and center of intersection std::vector<Pnt3r> intVerts; Pnt3r intCenter; intersectFrusta(eyeFrust, lightFrust, intVerts, intCenter); if(intVerts.empty() == true) return false; // xform intCenter and intVerts to post proj. light space matLightFull.multFull(intCenter, intCenter); std::vector<Pnt3r>::iterator ivIt = intVerts.begin(); std::vector<Pnt3r>::iterator ivEnd = intVerts.end (); for(; ivIt != ivEnd; ++ivIt) matLightFull.multFull(*ivIt, *ivIt); Pnt2r eyePos2D (eyePos [0], eyePos [1]); Pnt2r intCenter2D(intCenter[0], intCenter[1]); // center line, normal, direction and distance from origin Vec2r clDir (intCenter2D - eyePos2D); clDir.normalize(); Vec2r clNorm(-clDir[1], clDir[0]); // distance of the center line from the origin Real clDist = clNorm.dot(eyePos2D.subZero()); // compute top and base lines: // - project intVerts onto the center line. // - top line is perpendicular to center line and goes through the // projected point closest to eyePos // - base line is perpendicular to center line and goes through the // projected point farthest from eyePos Pnt2r tlBase; Pnt2r blBase; Real topDist = TypeTraits<Real>::getMax(); Real baseDist = TypeTraits<Real>::getMin(); std::vector<Pnt3r>::const_iterator ivCIt = intVerts.begin(); std::vector<Pnt3r>::const_iterator ivCEnd = intVerts.end (); for(; ivCIt != ivCEnd; ++ivCIt) { Pnt2r ivPnt((*ivCIt)[0], (*ivCIt)[1]); ivPnt = ivPnt - (clNorm.dot(ivPnt) - clDist) * clNorm; Real dist = (ivPnt - eyePos2D).squareLength(); dist *= osgSgn(clDir.dot(ivPnt - eyePos2D)); if(dist < topDist) { topDist = dist; tlBase = ivPnt; } if(dist > baseDist) { baseDist = dist; blBase = ivPnt; } } topDist = osgSgn(topDist ) * osgSqrt(osgAbs(topDist )); baseDist = osgSgn(baseDist) * osgSqrt(osgAbs(baseDist)); // compute side lines: // - choose focusPnt (everything closer to the near plane is mapped to // 80% of the shadow map) - here we just take the point at 0.7 between // tlBase and blBase // - find a point (trapTip, q in the paper) on center line such that // focusPnt is mapped the 80% line in the shadow map // - choose lines through q that touch the convex hull of intVerts ivCIt = intVerts.begin(); ivCEnd = intVerts.end (); // Real centerDist = (intCenter2D - eyePos2D).length(); Real lambda = baseDist - topDist; Real delta = 0.5f * lambda; Real xi = -0.6f; Real eta = ((lambda * delta) + (lambda * delta * xi)) / (lambda - 2.f * delta - lambda * xi ); Pnt2r trapTip = tlBase - (eta * clDir); Pnt2r focusPnt = tlBase + (delta * clDir); // on both sides of the center line, find the point in intVerts that has // the smallest |cosine| (largest angle) between clDir and the vector // from trapTip to intVerts[i] Pnt2r posPnt; Real posCos = 1.f; Pnt2r negPnt; Real negCos = 1.f; for(UInt32 i = 0; ivCIt != ivCEnd; ++ivCIt, ++i) { Pnt2r ivPnt((*ivCIt)[0], (*ivCIt)[1]); Vec2r v = ivPnt - trapTip; v.normalize(); Real currCos = osgAbs(clDir.dot(v)); if(clNorm.dot(v) >= 0.f) { if(currCos <= posCos) { posPnt = ivPnt; posCos = currCos; } } else { if(currCos <= negCos) { negPnt = ivPnt; negCos = currCos; } } } // compute corners of trapezoid: Pnt2r trapVerts [4]; Pnt2r extraVerts[2]; Real posTan = osgTan(osgACos(posCos)); Real negTan = osgTan(osgACos(negCos)); trapVerts[0] = blBase - ((eta + lambda) * negTan * clNorm); trapVerts[1] = blBase + ((eta + lambda) * posTan * clNorm); trapVerts[2] = tlBase + ( eta * posTan * clNorm); trapVerts[3] = tlBase - ( eta * negTan * clNorm); extraVerts[0] = focusPnt + ((eta + delta) * posTan * clNorm); extraVerts[1] = focusPnt - ((eta + delta) * negTan * clNorm); // == xform trapezoid to unit square == // M1 = R * T1 -- translate center of top line to origin and rotate Vec2r u = 0.5f * (trapVerts[2].subZero() + trapVerts[3].subZero()); Vec2r v = trapVerts[3] - trapVerts[2]; v.normalize(); matNT.setValue( v[0], v[1], 0.f, -(u[0] * v[0] + u[1] * v[1]), -v[1], v[0], 0.f, (u[0] * v[1] - u[1] * v[0]), 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 0.f, 1.f); // M2 = T2 * M1 -- translate tip to origin matNT[3][0] = - (matNT[0][0] * trapTip[0] + matNT[1][0] * trapTip[1]); matNT[3][1] = - (matNT[0][1] * trapTip[0] + matNT[1][1] * trapTip[1]); // M3 = H * M2 -- shear to make it symmetric wrt to the y axis // v = M2 * u v[0] = matNT[0][0] * u[0] + matNT[1][0] * u[1] + matNT[3][0]; v[1] = matNT[0][1] * u[0] + matNT[1][1] * u[1] + matNT[3][1]; Real a = - v[0] / v[1]; // matNT[*][0] : = mat[*][0] + a * mat[*][1] matNT[0][0] += a * matNT[0][1]; matNT[1][0] += a * matNT[1][1]; matNT[2][0] += a * matNT[2][1]; matNT[3][0] += a * matNT[3][1]; // M4 = S1 * M3 -- scale to make sidelines orthogonal and // top line is at y == 1 // v = 1 / (M3 * t2) v[0] = 1.f / (matNT[0][0] * trapVerts[2][0] + matNT[1][0] * trapVerts[2][1] + matNT[3][0]); v[1] = 1.f / (matNT[0][1] * trapVerts[2][0] + matNT[1][1] * trapVerts[2][1] + matNT[3][1]); matNT[0][0] *= v[0]; matNT[0][1] *= v[1]; matNT[1][0] *= v[0]; matNT[1][1] *= v[1]; matNT[2][0] *= v[0]; matNT[2][1] *= v[1]; matNT[3][0] *= v[0]; matNT[3][1] *= v[1]; // M5 = N * M4 -- turn trapezoid into rectangle matNT[0][3] = matNT[0][1]; matNT[1][3] = matNT[1][1]; matNT[2][3] = matNT[2][1]; matNT[3][3] = matNT[3][1]; matNT[3][1] += 1.f; // M6 = T3 * M5 -- translate center to origin // u = "M5 * t0" - only y and w coordinates // v = "M5 * t2" - only y and w coordinates u[0] = matNT[0][1] * trapVerts[0][0] + matNT[1][1] * trapVerts[0][1] + matNT[3][1]; u[1] = matNT[0][3] * trapVerts[0][0] + matNT[1][3] * trapVerts[0][1] + matNT[3][3]; v[0] = matNT[0][1] * trapVerts[2][0] + matNT[1][1] * trapVerts[2][1] + matNT[3][1]; v[1] = matNT[0][3] * trapVerts[2][0] + matNT[1][3] * trapVerts[2][1] + matNT[3][3]; a = - 0.5f * (u[0] / u[1] + v[0] / v[1]); matNT[0][1] += matNT[0][3] * a; matNT[1][1] += matNT[1][3] * a; matNT[2][1] += matNT[2][3] * a; matNT[3][1] += matNT[3][3] * a; // M7 = S2 * M6 -- scale to fill -1/+1 square // u = "M6 * t0" - only y and w coordinates u[0] = matNT[0][1] * trapVerts[0][0] + matNT[1][1] * trapVerts[0][1] + matNT[3][1]; u[1] = matNT[0][3] * trapVerts[0][0] + matNT[1][3] * trapVerts[0][1] + matNT[3][3]; a = -u[1] / u[0]; matNT[0][1] *= a; matNT[1][1] *= a; matNT[2][1] *= a; matNT[3][1] *= a; return true; }
void TrapezoidalShadowMapEngine::handlePointLightEnter( PointLight *pointL, RenderAction *ract, TSMEngineData *data) { RenderPartition *parentPart = ract->getActivePartition(); Matrixr matEyeToWorld(parentPart->getCameraToWorld()); Matrixr matLightProj; Real shadowNear = (getShadowNear() != 0.f ? getShadowNear() : parentPart->getNear() ); Real shadowFar = (getShadowFar () != 0.f ? getShadowFar () : parentPart->getFar() ); Inherited::calcPointLightRange( pointL, 0.01f, shadowNear, shadowFar, shadowNear, shadowFar); MatrixPerspective(matLightProj, Pi / 4.f, 1.f, shadowNear, shadowFar ); Matrixr matWorldToLight; Matrixr matEyeToLight; MFMatrixr mfMatNT; mfMatNT.resize(6); Inherited::calcPointLightMatrices(matWorldToLight, matEyeToLight, pointL, matEyeToWorld ); Inherited::updatePointLightShadowTexImage (data); Inherited::updatePointLightShadowTexBuffers(data); Inherited::updatePointLightRenderTargets (data); Int32 shadowTexUnit = (this->getForceTextureUnit() >= 0) ? this->getForceTextureUnit() : 7; ShaderProgram *shadowFP = this->getShadowFragmentProgram(); if(shadowFP == NULL) { ShaderProgramUnrecPtr newShadowFP = ShaderProgram::createLocal(); newShadowFP->setShaderType(GL_FRAGMENT_SHADER); newShadowFP->setProgram (_pointFPCode ); newShadowFP->addUniformVariable("TSME_matEyeToLight", matEyeToLight); newShadowFP->addUniformVariable("TSME_matLightProj", matLightProj ); newShadowFP->addUniformVariable("TSME_matNT", mfMatNT ); newShadowFP->addUniformVariable("TSME_texShadow", shadowTexUnit); newShadowFP->addUniformVariable("TSME_texShadowSizeInv", Vec2f(1.f / getWidth (), 1.f / getHeight() ) ); this->setShadowFragmentProgram(newShadowFP); shadowFP = newShadowFP; } else { shadowFP->updateUniformVariable("TSME_matEyeToLight", matEyeToLight); shadowFP->updateUniformVariable("TSME_matLightProj", matLightProj ); } const FrustumVolume &eyeFrust = parentPart->getFrustum(); for(UInt16 faceIdx = 0; faceIdx < 6; ++faceIdx) { Matrixr matWorldToLightFace (matWorldToLight ); matWorldToLightFace.multLeft(_matCubeFaceInv[faceIdx]); Matrixr matLightFull(matWorldToLightFace); matLightFull.multLeft(matLightProj); FrustumVolume lightFrust; Matrixr matNT; lightFrust.setPlanes(matLightFull); bool matNTValid = calcTrapezoidalTransform(mfMatNT[faceIdx], matEyeToWorld, matLightFull, eyeFrust, lightFrust ); if(matNTValid == false) { // setup a minimal partition to clear the cube face commitChanges(); this->pushPartition(ract, RenderPartition::CopyNothing, RenderPartition::SimpleCallback); { RenderPartition *part = ract->getActivePartition( ); Window *win = ract->getWindow ( ); FrameBufferObject *target = data->getRenderTargets (faceIdx); Background *back = data->getBackground ( ); part->setSetupMode(RenderPartition::ViewportSetup | RenderPartition::BackgroundSetup ); part->setRenderTarget(target); part->setWindow (win ); part->calcViewportDimension(0.f, 0.f, 1.f, 1.f, target->getWidth (), target->getHeight() ); part->setBackground(back); RenderPartition::SimpleDrawCallback emptyCubeFaceDraw = boost::bind( &TrapezoidalShadowMapEngine::emptyCubeFaceDrawFunc, this, _1); part->dropFunctor(emptyCubeFaceDraw); } this->popPartition(ract); } else { updateLightPassMaterial(data, faceIdx, mfMatNT[faceIdx]); commitChanges(); this->pushPartition(ract); { RenderPartition *part = ract->getActivePartition( ); Window *win = ract->getWindow ( ); FrameBufferObject *target = data->getRenderTargets (faceIdx); Background *back = data->getBackground ( ); part->setRenderTarget(target); part->setWindow (win ); part->calcViewportDimension(0.f, 0.f, 1.f, 1.f, target->getWidth (), target->getHeight() ); part->setupProjection(matLightProj, Matrixr::identity()); part->setupViewing (matWorldToLightFace ); part->setNear (parentPart->getNear()); part->setFar (parentPart->getFar ()); part->setFrustum (lightFrust ); part->setBackground (back ); part->overrideMaterial(data->getLightPassMaterials(faceIdx), ract->getActNode ( ) ); this->recurseFrom(ract, pointL); ract->useNodeList(false ); part->overrideMaterial(NULL, ract->getActNode ( ) ); } this->popPartition(ract); } } shadowFP->updateUniformVariable("TSME_matNT", mfMatNT); }
void ReplicateTransform::accumulateMatrix(Matrixr &result) { result.mult(_invWorld); }
void ShaderShadowMapEngine::handleDirectionalLightEnter( DirectionalLight *dirL, RenderAction *ract, SSMEngineData *data) { RenderPartition *parentPart = ract ->getActivePartition(); FrustumVolume camFrust = parentPart->getFrustum (); Matrixr matEyeToWorld (parentPart->getCameraToWorld()); Matrixr matWorldToLight; Matrixr matEyeToLight; calcDirectionalLightMatrices(matWorldToLight, matEyeToLight, dirL, matEyeToWorld ); // place light camera outside the scene bounding box: // - project camera frustum and scene bounding box into a // coordinate system where the directional light shines // along the -z axis. // - compute 2 AABBs that contain the projected frustum and // scene BB // - width and height of the ortho projection are determined from // the frustum AABB, while near and far are determined by the // scene AABB (offscreen objects cast shadows into the view volume) Pnt3r camVerts [10]; Pnt3r sceneVerts[10]; const Matrix &matSceneToWorld = ract->topMatrix (); BoxVolume sceneBB = ract->getActNode()->getVolume(); camFrust.getCorners(camVerts [0], camVerts [1], camVerts [2], camVerts [3], camVerts [4], camVerts [5], camVerts [6], camVerts [7] ); sceneBB .getCorners(sceneVerts[0], sceneVerts[1], sceneVerts[2], sceneVerts[3], sceneVerts[4], sceneVerts[5], sceneVerts[6], sceneVerts[7] ); camVerts [8].setValues(TypeTraits<Real>::getMax(), TypeTraits<Real>::getMax(), TypeTraits<Real>::getMax() ); camVerts [9].setValues(TypeTraits<Real>::getMin(), TypeTraits<Real>::getMin(), TypeTraits<Real>::getMin() ); sceneVerts[8].setValues(TypeTraits<Real>::getMax(), TypeTraits<Real>::getMax(), TypeTraits<Real>::getMax() ); sceneVerts[9].setValues(TypeTraits<Real>::getMin(), TypeTraits<Real>::getMin(), TypeTraits<Real>::getMin() ); for(UInt32 i = 0; i < 8; ++i) { matWorldToLight.mult(camVerts [i], camVerts [i]); matSceneToWorld.mult(sceneVerts[i], sceneVerts[i]); matWorldToLight.mult(sceneVerts[i], sceneVerts[i]); camVerts [8][0] = osgMin(camVerts [8][0], camVerts [i][0]); camVerts [9][0] = osgMax(camVerts [9][0], camVerts [i][0]); camVerts [8][1] = osgMin(camVerts [8][1], camVerts [i][1]); camVerts [9][1] = osgMax(camVerts [9][1], camVerts [i][1]); sceneVerts[8][0] = osgMin(sceneVerts[8][0], sceneVerts[i][0]); sceneVerts[9][0] = osgMax(sceneVerts[9][0], sceneVerts[i][0]); sceneVerts[8][1] = osgMin(sceneVerts[8][1], sceneVerts[i][1]); sceneVerts[9][1] = osgMax(sceneVerts[9][1], sceneVerts[i][1]); sceneVerts[8][2] = osgMin(sceneVerts[8][2], sceneVerts[i][2]); sceneVerts[9][2] = osgMax(sceneVerts[9][2], sceneVerts[i][2]); } // these points are the corners of the ortho shadow view volume Pnt3r lightMin(osgMax(camVerts[8][0], sceneVerts[8][0]), osgMax(camVerts[8][1], sceneVerts[8][1]), -sceneVerts[9][2]); Pnt3r lightMax(osgMin(camVerts[9][0], sceneVerts[9][0]), osgMin(camVerts[9][1], sceneVerts[9][1]), -sceneVerts[8][2]); // enlarge by 2% in x, y, z direction lightMin[0] -= (lightMax[0] - lightMin[0]) * 0.01f; lightMin[1] -= (lightMax[1] - lightMin[1]) * 0.01f; lightMin[2] -= (lightMax[2] - lightMin[2]) * 0.01f; lightMax[0] += (lightMax[0] - lightMin[0]) * 0.01f; lightMax[1] += (lightMax[1] - lightMin[1]) * 0.01f; lightMax[2] += (lightMax[2] - lightMin[2]) * 0.01f; Matrixr matLightProj; Matrixr matLightProjTrans; MatrixOrthogonal(matLightProj, lightMin[0], lightMax[0], lightMin[1], lightMax[1], lightMin[2], lightMax[2] ); updateShadowTexImage (data); updateShadowTexBuffers(data); updateRenderTargets (data); Int32 shadowTexUnit = (this->getForceTextureUnit() > 0) ? this->getForceTextureUnit() : 7; ShaderProgram *shadowFP = this->getShadowFragmentProgram(); if(shadowFP == NULL) { ShaderProgramUnrecPtr newShadowFP = ShaderProgram::createLocal(); newShadowFP->setShaderType(GL_FRAGMENT_SHADER); newShadowFP->setProgram (_dirFPCode ); newShadowFP->addUniformVariable("SSME_matEyeToLight", matEyeToLight); newShadowFP->addUniformVariable("SSME_matLightProj", matLightProj ); newShadowFP->addUniformVariable("SSME_texShadow", shadowTexUnit); this->setShadowFragmentProgram(newShadowFP); shadowFP = newShadowFP; } else { shadowFP->updateUniformVariable("SSME_matEyeToLight", matEyeToLight); shadowFP->updateUniformVariable("SSME_matLightProj", matLightProj ); } commitChanges(); this->pushPartition(ract); { RenderPartition *part = ract->getActivePartition( ); Window *win = ract->getWindow ( ); FrameBufferObject *target = data->getRenderTargets (0); Background *back = data->getBackground ( ); part->setRenderTarget(target); part->setWindow (win ); part->calcViewportDimension(0.f, 0.f, 1.f, 1.f, target->getWidth (), target->getHeight() ); part->setupProjection(matLightProj, matLightProjTrans); part->setupViewing (matWorldToLight ); part->setNear (parentPart->getNear()); part->setFar (parentPart->getFar ()); part->calcFrustum ( ); part->setBackground (back ); // force material for shadow map generation part->overrideMaterial(data->getLightPassMaterials(0), ract->getActNode ( ) ); this->recurseFrom(ract, dirL); ract->useNodeList(false ); // undo override part->overrideMaterial(NULL, ract->getActNode ( ) ); } this->popPartition(ract); }
static inline void setGenFunc( GLenum coord, GLenum gen, GLenum func, const Vec4f &plane, Node *beacon, Matrix &cameraMat, UInt32 eyeMode, Matrix &eyeMatrix) { #ifndef OSG_EMBEDDED if(beacon != NULL) { Matrixr beaconMat; beacon->getToWorld(beaconMat); beaconMat.multLeft(cameraMat); glPushMatrix(); glLoadMatrixf(beaconMat.getValues()); glTexGenfv(coord, GL_EYE_PLANE, const_cast<GLfloat *>(plane.getValues())); glTexGeni(coord, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glPopMatrix(); glEnable(gen); } else if(func == GL_EYE_LINEAR) { glPushMatrix(); switch(eyeMode) { case TexGenChunk::EyeModelViewIdentity: glLoadIdentity(); break; case TexGenChunk::EyeModelViewStored: glLoadMatrixf(eyeMatrix.getValues()); break; case TexGenChunk::EyeModelViewCamera: glLoadMatrixf(cameraMat.getValues()); break; default: break; } glTexGenfv(coord, GL_EYE_PLANE, const_cast<GLfloat *>(plane.getValues())); glTexGeni(coord, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glPopMatrix(); glEnable(gen); } else if(func != GL_NONE) { glTexGeni(coord, GL_TEXTURE_GEN_MODE, func); if(func == GL_OBJECT_LINEAR) { glTexGenfv(coord, GL_OBJECT_PLANE, const_cast<GLfloat *>(plane.getValues())); } glEnable(gen); } #endif }