2002-06-12 21:54:14 +08:00
|
|
|
#include <osg/ShadowVolumeOccluder>
|
2002-06-14 00:21:00 +08:00
|
|
|
#include <osg/CullStack>
|
2002-05-29 07:43:22 +08:00
|
|
|
|
2002-06-14 07:46:02 +08:00
|
|
|
#include <osg/Group>
|
|
|
|
#include <osg/Geode>
|
|
|
|
#include <osg/GeoSet>
|
|
|
|
|
2002-05-29 07:43:22 +08:00
|
|
|
using namespace osg;
|
2002-06-03 23:39:41 +08:00
|
|
|
|
|
|
|
|
2002-06-14 00:21:00 +08:00
|
|
|
|
2002-06-14 19:27:34 +08:00
|
|
|
typedef std::pair<unsigned int,Vec3> Point; // bool=true signifies a newly created point, false indicates original point.
|
2002-06-14 00:21:00 +08:00
|
|
|
typedef std::vector<Point> PointList;
|
|
|
|
typedef std::vector<Vec3> VertexList;
|
|
|
|
|
|
|
|
|
2002-06-19 06:35:48 +08:00
|
|
|
// copyVertexListToPointList a vector for Vec3 into a vector of Point's.
|
|
|
|
void copyVertexListToPointList(const VertexList& in,PointList& out)
|
2002-06-03 23:39:41 +08:00
|
|
|
{
|
2002-06-14 00:21:00 +08:00
|
|
|
out.reserve(in.size());
|
|
|
|
for(VertexList::const_iterator itr=in.begin();
|
|
|
|
itr!=in.end();
|
|
|
|
++itr)
|
|
|
|
{
|
2002-06-14 19:27:34 +08:00
|
|
|
out.push_back(Point(0,*itr));
|
2002-06-14 00:21:00 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2002-06-19 06:35:48 +08:00
|
|
|
void copyPointListToVertexList(const PointList& in,VertexList& out)
|
|
|
|
{
|
|
|
|
out.reserve(in.size());
|
|
|
|
for(PointList::const_iterator itr=in.begin();
|
|
|
|
itr!=in.end();
|
|
|
|
++itr)
|
|
|
|
{
|
|
|
|
out.push_back(itr->second);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2002-06-14 00:21:00 +08:00
|
|
|
// clip the convex hull 'in' to plane to generate a clipped convex hull 'out'
|
|
|
|
// return true if points remain after clipping.
|
2002-06-14 19:27:34 +08:00
|
|
|
unsigned int clip(const Plane& plane,const PointList& in, PointList& out,unsigned int planeMask)
|
2002-06-14 00:21:00 +08:00
|
|
|
{
|
|
|
|
std::vector<float> distance;
|
|
|
|
distance.reserve(in.size());
|
|
|
|
for(PointList::const_iterator itr=in.begin();
|
|
|
|
itr!=in.end();
|
|
|
|
++itr)
|
|
|
|
{
|
|
|
|
distance.push_back(plane.distance(itr->second));
|
|
|
|
}
|
|
|
|
|
|
|
|
out.clear();
|
|
|
|
|
|
|
|
for(unsigned int i=0;i<in.size();++i)
|
|
|
|
{
|
|
|
|
unsigned int i_1 = (i+1)%in.size(); // do the mod to wrap the index round back to the start.
|
|
|
|
|
|
|
|
if (distance[i]>=0.0f)
|
|
|
|
{
|
|
|
|
out.push_back(in[i]);
|
|
|
|
|
2002-06-14 19:27:34 +08:00
|
|
|
|
2002-06-14 00:21:00 +08:00
|
|
|
if (distance[i_1]<0.0f)
|
|
|
|
{
|
2002-06-14 19:27:34 +08:00
|
|
|
unsigned int mask = (in[i].first & in[i_1].first) | planeMask;
|
2002-06-14 00:21:00 +08:00
|
|
|
float r = distance[i_1]/(distance[i_1]-distance[i]);
|
2002-06-14 19:27:34 +08:00
|
|
|
out.push_back(Point(mask,in[i].second*r+in[i_1].second*(1.0f-r)));
|
2002-06-14 00:21:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
else if (distance[i_1]>0.0f)
|
|
|
|
{
|
2002-06-14 19:27:34 +08:00
|
|
|
unsigned int mask = (in[i].first & in[i_1].first) | planeMask;
|
2002-06-14 00:21:00 +08:00
|
|
|
float r = distance[i_1]/(distance[i_1]-distance[i]);
|
2002-06-14 19:27:34 +08:00
|
|
|
out.push_back(Point(mask,in[i].second*r+in[i_1].second*(1.0f-r)));
|
2002-06-14 00:21:00 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return out.size();
|
|
|
|
}
|
|
|
|
|
|
|
|
// clip the convex hull 'in' to planeList to generate a clipped convex hull 'out'
|
|
|
|
// return true if points remain after clipping.
|
|
|
|
unsigned int clip(const Polytope::PlaneList& planeList,const VertexList& vin,PointList& out)
|
|
|
|
{
|
|
|
|
PointList in;
|
2002-06-19 06:35:48 +08:00
|
|
|
copyVertexListToPointList(vin,in);
|
2002-06-14 00:21:00 +08:00
|
|
|
|
2002-06-14 19:27:34 +08:00
|
|
|
unsigned int planeMask = 0x1;
|
2002-06-14 00:21:00 +08:00
|
|
|
for(Polytope::PlaneList::const_iterator itr=planeList.begin();
|
|
|
|
itr!=planeList.end();
|
|
|
|
++itr)
|
|
|
|
{
|
2002-06-14 19:27:34 +08:00
|
|
|
if (!clip(*itr,in,out,planeMask)) return false;
|
2002-06-14 00:21:00 +08:00
|
|
|
in.swap(out);
|
2002-06-14 19:27:34 +08:00
|
|
|
planeMask <<= 1;
|
2002-06-14 00:21:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
in.swap(out);
|
|
|
|
|
|
|
|
return out.size();
|
|
|
|
}
|
|
|
|
|
|
|
|
void transform(PointList& points,const osg::Matrix& matrix)
|
|
|
|
{
|
|
|
|
for(PointList::iterator itr=points.begin();
|
|
|
|
itr!=points.end();
|
|
|
|
++itr)
|
|
|
|
{
|
|
|
|
itr->second = itr->second*matrix;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void transform(const PointList& in,PointList& out,const osg::Matrix& matrix)
|
|
|
|
{
|
|
|
|
for(PointList::const_iterator itr=in.begin();
|
|
|
|
itr!=in.end();
|
|
|
|
++itr)
|
|
|
|
{
|
|
|
|
out.push_back(Point(itr->first,itr->second * matrix));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void pushToFarPlane(PointList& points)
|
|
|
|
{
|
|
|
|
for(PointList::iterator itr=points.begin();
|
|
|
|
itr!=points.end();
|
|
|
|
++itr)
|
|
|
|
{
|
2002-06-14 19:27:34 +08:00
|
|
|
itr->second.z() = 1.0f;
|
2002-06-14 00:21:00 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void computePlanes(const PointList& front, const PointList& back, Polytope::PlaneList& planeList)
|
|
|
|
{
|
|
|
|
for(unsigned int i=0;i<front.size();++i)
|
|
|
|
{
|
|
|
|
unsigned int i_1 = (i+1)%front.size(); // do the mod to wrap the index round back to the start.
|
2002-06-14 19:27:34 +08:00
|
|
|
if (!(front[i].first & front[i_1].first))
|
2002-06-14 00:21:00 +08:00
|
|
|
{
|
|
|
|
planeList.push_back(Plane(front[i].second,front[i_1].second,back[i].second));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Plane computeFrontPlane(const PointList& front)
|
|
|
|
{
|
|
|
|
return Plane(front[2].second,front[1].second,front[0].second);
|
|
|
|
}
|
|
|
|
|
2002-06-15 00:28:47 +08:00
|
|
|
// compute the volume between the front and back polygons of the occluder/hole.
|
2002-06-16 04:57:50 +08:00
|
|
|
float computePolytopeVolume(const PointList& front, const PointList& back)
|
2002-06-15 00:28:47 +08:00
|
|
|
{
|
|
|
|
float volume = 0.0f;
|
|
|
|
Vec3 frontStart = front[0].second;
|
|
|
|
Vec3 backStart = back[0].second;
|
|
|
|
for(unsigned int i=1;i<front.size()-1;++i)
|
|
|
|
{
|
|
|
|
volume += computeVolume(frontStart, front[i].second, front[i+1].second,
|
|
|
|
backStart, back[i].second, back[i+1].second);
|
|
|
|
}
|
|
|
|
return volume;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2002-06-14 07:46:02 +08:00
|
|
|
Drawable* createOccluderDrawable(const PointList& front, const PointList& back)
|
|
|
|
{
|
|
|
|
// create a drawable for occluder.
|
|
|
|
osg::GeoSet* geoset = osgNew osg::GeoSet;
|
|
|
|
|
|
|
|
int totalNumber = front.size()+back.size();
|
|
|
|
osg::Vec3* coords = osgNew osg::Vec3[front.size()+back.size()];
|
|
|
|
osg::Vec3* cptr = coords;
|
|
|
|
for(PointList::const_iterator fitr=front.begin();
|
|
|
|
fitr!=front.end();
|
|
|
|
++fitr)
|
|
|
|
{
|
|
|
|
*cptr = fitr->second;
|
|
|
|
++cptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
for(PointList::const_iterator bitr=back.begin();
|
|
|
|
bitr!=back.end();
|
|
|
|
++bitr)
|
|
|
|
{
|
|
|
|
*cptr = bitr->second;
|
|
|
|
++cptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
geoset->setCoords(coords);
|
|
|
|
|
|
|
|
osg::Vec4* color = osgNew osg::Vec4[1];
|
|
|
|
color[0].set(1.0f,1.0f,1.0f,0.5f);
|
|
|
|
geoset->setColors(color);
|
|
|
|
geoset->setColorBinding(osg::GeoSet::BIND_OVERALL);
|
|
|
|
|
|
|
|
geoset->setPrimType(osg::GeoSet::POINTS);
|
|
|
|
geoset->setNumPrims(totalNumber);
|
|
|
|
|
|
|
|
//cout << "totalNumber = "<<totalNumber<<endl;
|
|
|
|
|
|
|
|
|
|
|
|
osg::Geode* geode = osgNew osg::Geode;
|
|
|
|
geode->addDrawable(geoset);
|
|
|
|
|
|
|
|
osg::StateSet* stateset = osgNew osg::StateSet;
|
|
|
|
stateset->setMode(GL_LIGHTING,osg::StateAttribute::OFF);
|
|
|
|
stateset->setMode(GL_BLEND,osg::StateAttribute::ON);
|
|
|
|
stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
|
|
|
|
|
|
|
|
geoset->setStateSet(stateset);
|
|
|
|
|
|
|
|
return geoset;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool ShadowVolumeOccluder::computeOccluder(const NodePath& nodePath,const ConvexPlanerOccluder& occluder,CullStack& cullStack,bool createDrawables)
|
2002-06-14 00:21:00 +08:00
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
// std::cout<<" Computing Occluder"<<std::endl;
|
|
|
|
|
|
|
|
CullingSet& cullingset = cullStack.getCurrentCullingSet();
|
|
|
|
|
|
|
|
const Matrix& MV = cullStack.getModelViewMatrix();
|
|
|
|
const Matrix& P = cullStack.getProjectionMatrix();
|
|
|
|
|
|
|
|
// take a reference to the NodePath to this occluder.
|
|
|
|
_nodePath = nodePath;
|
|
|
|
|
|
|
|
// take a reference to the projection matrix.
|
|
|
|
_projectionMatrix = &P;
|
|
|
|
|
2002-06-15 00:28:47 +08:00
|
|
|
// initialize the volume
|
|
|
|
_volume = 0.0f;
|
2002-06-14 00:21:00 +08:00
|
|
|
|
|
|
|
|
|
|
|
// compute the inverse of the projection matrix.
|
|
|
|
Matrix invP;
|
|
|
|
invP.invert(P);
|
|
|
|
|
2002-06-15 20:14:42 +08:00
|
|
|
float volumeview = cullStack.getFrustumVolume();
|
2002-06-15 00:28:47 +08:00
|
|
|
|
2002-06-14 07:46:02 +08:00
|
|
|
|
2002-06-14 00:21:00 +08:00
|
|
|
// compute the transformation matrix which takes form local coords into clip space.
|
2002-06-14 07:46:02 +08:00
|
|
|
Matrix MVP(MV*P);
|
2002-06-12 21:54:14 +08:00
|
|
|
|
|
|
|
// for the occluder polygon and each of the holes do
|
|
|
|
// first transform occluder polygon into clipspace by multiple it by c[i] = v[i]*(MV*P)
|
|
|
|
// then push to coords to far plane by setting its coord to c[i].z = -1.
|
|
|
|
// then transform far plane polygon back into projection space, by p[i]*inv(P)
|
|
|
|
// compute orientation of front plane, if normal.z()<0 then facing away from eye pont, so reverse the polygons, or simply invert planes.
|
|
|
|
// compute volume (quality) betwen front polygon in projection space and back polygon in projection space.
|
|
|
|
|
|
|
|
|
2002-06-14 00:21:00 +08:00
|
|
|
const VertexList& vertices_in = occluder.getOccluder().getVertexList();
|
|
|
|
|
|
|
|
PointList points;
|
2002-06-12 21:54:14 +08:00
|
|
|
|
2002-06-14 00:21:00 +08:00
|
|
|
if (clip(cullingset.getFrustum().getPlaneList(),vertices_in,points)>=3)
|
|
|
|
{
|
|
|
|
// compute the points on the far plane.
|
|
|
|
PointList farPoints;
|
|
|
|
farPoints.reserve(points.size());
|
|
|
|
transform(points,farPoints,MVP);
|
|
|
|
pushToFarPlane(farPoints);
|
|
|
|
transform(farPoints,invP);
|
|
|
|
|
|
|
|
// move the occlude points into projection space.
|
|
|
|
transform(points,MV);
|
|
|
|
|
2002-06-19 06:35:48 +08:00
|
|
|
// use the points on the front plane as reference vertices on the _occluderVolume
|
|
|
|
// so that the vertices can later by used to test for occlusion of the occluder itself.
|
|
|
|
copyPointListToVertexList(points,_occluderVolume.getReferenceVertexList());
|
|
|
|
|
2002-06-14 00:21:00 +08:00
|
|
|
// create the front face of the occluder
|
|
|
|
Plane occludePlane = computeFrontPlane(points);
|
|
|
|
_occluderVolume.add(occludePlane);
|
2002-06-14 19:27:34 +08:00
|
|
|
|
|
|
|
// create the sides of the occluder
|
|
|
|
computePlanes(points,farPoints,_occluderVolume.getPlaneList());
|
|
|
|
|
|
|
|
_occluderVolume.setupMask();
|
2002-06-14 00:21:00 +08:00
|
|
|
|
|
|
|
// if the front face is pointing away from the eye point flip the whole polytope.
|
|
|
|
if (occludePlane[3]>0.0f)
|
|
|
|
{
|
|
|
|
_occluderVolume.flip();
|
|
|
|
}
|
2002-06-15 00:28:47 +08:00
|
|
|
|
2002-06-16 04:57:50 +08:00
|
|
|
_volume = computePolytopeVolume(points,farPoints)/volumeview;
|
2002-06-14 07:46:02 +08:00
|
|
|
|
|
|
|
if (createDrawables && !nodePath.empty())
|
2002-06-14 00:21:00 +08:00
|
|
|
{
|
2002-06-14 07:46:02 +08:00
|
|
|
osg::Group* group = dynamic_cast<osg::Group*>(nodePath.back());
|
|
|
|
if (group)
|
|
|
|
{
|
|
|
|
|
|
|
|
osg::Matrix invMV;
|
|
|
|
invMV.invert(MV);
|
|
|
|
|
|
|
|
transform(points,invMV);
|
|
|
|
transform(farPoints,invMV);
|
|
|
|
|
|
|
|
osg::Geode* geode = osgNew osg::Geode;
|
|
|
|
group->addChild(geode);
|
|
|
|
geode->addDrawable(createOccluderDrawable(points,farPoints));
|
|
|
|
}
|
2002-06-14 00:21:00 +08:00
|
|
|
}
|
2002-06-14 21:49:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
for(ConvexPlanerOccluder::HoleList::const_iterator hitr=occluder.getHoleList().begin();
|
|
|
|
hitr!=occluder.getHoleList().end();
|
|
|
|
++hitr)
|
|
|
|
{
|
|
|
|
PointList points;
|
|
|
|
if (clip(cullingset.getFrustum().getPlaneList(),hitr->getVertexList(),points)>=3)
|
|
|
|
{
|
2002-06-16 04:57:50 +08:00
|
|
|
_holeList.push_back(Polytope());
|
2002-06-14 21:49:59 +08:00
|
|
|
Polytope& polytope = _holeList.back();
|
|
|
|
|
|
|
|
// compute the points on the far plane.
|
|
|
|
PointList farPoints;
|
|
|
|
farPoints.reserve(points.size());
|
|
|
|
transform(points,farPoints,MVP);
|
|
|
|
pushToFarPlane(farPoints);
|
|
|
|
transform(farPoints,invP);
|
|
|
|
|
|
|
|
// move the occlude points into projection space.
|
|
|
|
transform(points,MV);
|
|
|
|
|
2002-06-19 06:35:48 +08:00
|
|
|
// use the points on the front plane as reference vertices on the _occluderVolume
|
|
|
|
// so that the vertices can later by used to test for occlusion of the occluder itself.
|
|
|
|
copyPointListToVertexList(points,polytope.getReferenceVertexList());
|
|
|
|
|
2002-06-14 21:49:59 +08:00
|
|
|
// create the front face of the occluder
|
|
|
|
Plane occludePlane = computeFrontPlane(points);
|
|
|
|
|
|
|
|
// create the sides of the occluder
|
|
|
|
computePlanes(points,farPoints,polytope.getPlaneList());
|
|
|
|
|
|
|
|
polytope.setupMask();
|
|
|
|
|
|
|
|
// if the front face is pointing away from the eye point flip the whole polytope.
|
|
|
|
if (occludePlane[3]>0.0f)
|
|
|
|
{
|
|
|
|
polytope.flip();
|
|
|
|
}
|
|
|
|
|
2002-06-15 00:28:47 +08:00
|
|
|
// remove the hole's volume from the occluder volume.
|
2002-06-16 04:57:50 +08:00
|
|
|
_volume -= computePolytopeVolume(points,farPoints)/volumeview;
|
2002-06-15 00:28:47 +08:00
|
|
|
|
2002-06-14 21:49:59 +08:00
|
|
|
if (createDrawables && !nodePath.empty())
|
|
|
|
{
|
|
|
|
osg::Group* group = dynamic_cast<osg::Group*>(nodePath.back());
|
|
|
|
if (group)
|
|
|
|
{
|
|
|
|
|
|
|
|
osg::Matrix invMV;
|
|
|
|
invMV.invert(MV);
|
|
|
|
|
|
|
|
transform(points,invMV);
|
|
|
|
transform(farPoints,invMV);
|
|
|
|
|
|
|
|
osg::Geode* geode = osgNew osg::Geode;
|
|
|
|
group->addChild(geode);
|
|
|
|
geode->addDrawable(createOccluderDrawable(points,farPoints));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2002-06-14 00:21:00 +08:00
|
|
|
|
2002-06-15 20:14:42 +08:00
|
|
|
//std::cout << "final volume = "<<_volume<<std::endl;
|
2002-06-14 00:21:00 +08:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
2002-06-12 17:22:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
bool ShadowVolumeOccluder::contains(const std::vector<Vec3>& vertices)
|
|
|
|
{
|
|
|
|
if (_occluderVolume.containsAllOf(vertices))
|
|
|
|
{
|
|
|
|
for(HoleList::iterator itr=_holeList.begin();
|
|
|
|
itr!=_holeList.end();
|
|
|
|
++itr)
|
|
|
|
{
|
2002-06-19 18:19:10 +08:00
|
|
|
PointList points;
|
|
|
|
if (clip(itr->getPlaneList(),vertices,points)>=3) return false;
|
2002-06-12 17:22:30 +08:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
2002-06-03 23:39:41 +08:00
|
|
|
}
|
|
|
|
|
2002-06-10 19:21:21 +08:00
|
|
|
bool ShadowVolumeOccluder::contains(const BoundingSphere& bound)
|
2002-06-03 23:39:41 +08:00
|
|
|
{
|
2002-06-17 17:10:26 +08:00
|
|
|
//std::cout << "Sphere testing occluder "<<this<<" mask="<<_occluderVolume.getCurrentMask();
|
2002-06-03 23:39:41 +08:00
|
|
|
if (_occluderVolume.containsAllOf(bound))
|
|
|
|
{
|
|
|
|
for(HoleList::iterator itr=_holeList.begin();
|
|
|
|
itr!=_holeList.end();
|
|
|
|
++itr)
|
|
|
|
{
|
2002-06-17 17:10:26 +08:00
|
|
|
if (itr->contains(bound))
|
|
|
|
{
|
|
|
|
//std::cout << " - not in occluder"<<std::endl;
|
|
|
|
return false;
|
|
|
|
}
|
2002-06-03 23:39:41 +08:00
|
|
|
}
|
2002-06-17 17:10:26 +08:00
|
|
|
//std::cout << " - in occluder ******"<<std::endl;
|
2002-06-03 23:39:41 +08:00
|
|
|
return true;
|
|
|
|
}
|
2002-06-17 17:10:26 +08:00
|
|
|
//std::cout << " - not in occluder"<<std::endl;
|
2002-06-03 23:39:41 +08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2002-06-10 19:21:21 +08:00
|
|
|
bool ShadowVolumeOccluder::contains(const BoundingBox& bound)
|
2002-06-03 23:39:41 +08:00
|
|
|
{
|
2002-06-17 17:10:26 +08:00
|
|
|
//std::cout << "Box testing occluder "<<this<<" mask="<<_occluderVolume.getCurrentMask();
|
2002-06-03 23:39:41 +08:00
|
|
|
if (_occluderVolume.containsAllOf(bound))
|
|
|
|
{
|
|
|
|
for(HoleList::iterator itr=_holeList.begin();
|
|
|
|
itr!=_holeList.end();
|
|
|
|
++itr)
|
|
|
|
{
|
2002-06-17 17:10:26 +08:00
|
|
|
if (itr->contains(bound))
|
|
|
|
{
|
|
|
|
//std::cout << " + not in occluder"<<std::endl;
|
|
|
|
return false;
|
|
|
|
}
|
2002-06-03 23:39:41 +08:00
|
|
|
}
|
2002-06-17 17:10:26 +08:00
|
|
|
//std::cout << "+ in occluder ********"<<std::endl;
|
2002-06-03 23:39:41 +08:00
|
|
|
return true;
|
|
|
|
}
|
2002-06-17 17:10:26 +08:00
|
|
|
//std::cout << "+ not in occluder"<<std::endl;
|
2002-06-03 23:39:41 +08:00
|
|
|
return false;
|
|
|
|
}
|