Updates for improvements to osgdem, such as adding support for skirt
and border into osg::HeightField, handling of computation of neigherbouring tiles in osgdem's DestinationGraph.
This commit is contained in:
parent
c06c73993e
commit
b9f032bbb5
@ -17,6 +17,7 @@
|
||||
#include <osg/ShapeDrawable>
|
||||
#include <osg/Texture2D>
|
||||
#include <osg/Group>
|
||||
#include <osg/Geometry>
|
||||
|
||||
#include <osgDB/ReadFile>
|
||||
#include <osgDB/WriteFile>
|
||||
@ -772,6 +773,7 @@ void DataSet::Source::buildOverviews()
|
||||
|
||||
|
||||
DataSet::DestinationTile::DestinationTile():
|
||||
_dataSet(0),
|
||||
_imagery_maxNumColumns(4096),
|
||||
_imagery_maxNumRows(4096),
|
||||
_imagery_maxSourceResolutionX(0.0f),
|
||||
@ -918,6 +920,17 @@ void DataSet::DestinationTile::allocate()
|
||||
|
||||
}
|
||||
|
||||
void DataSet::DestinationTile::computeNeighboursFromQuadMap()
|
||||
{
|
||||
if (_dataSet)
|
||||
{
|
||||
setNeighbours(_dataSet->getTile(_level,_X-1,_Y),_dataSet->getTile(_level,_X-1,_Y-1),
|
||||
_dataSet->getTile(_level,_X,_Y-1),_dataSet->getTile(_level,_X+1,_Y-1),
|
||||
_dataSet->getTile(_level,_X+1,_Y),_dataSet->getTile(_level,_X+1,_Y+1),
|
||||
_dataSet->getTile(_level,_X,_Y+1),_dataSet->getTile(_level,_X-1,_Y+1));
|
||||
}
|
||||
}
|
||||
|
||||
void DataSet::DestinationTile::setNeighbours(DestinationTile* left, DestinationTile* left_below,
|
||||
DestinationTile* below, DestinationTile* below_right,
|
||||
DestinationTile* right, DestinationTile* right_above,
|
||||
@ -1302,8 +1315,12 @@ osg::Node* DataSet::DestinationTile::createScene()
|
||||
{
|
||||
if (_terrain.valid() && _terrain->_heightField.valid())
|
||||
{
|
||||
osg::HeightField* hf = _terrain->_heightField.get();
|
||||
|
||||
hf->setSkirtHeight(0.003f);
|
||||
|
||||
osg::Geode* geode = new osg::Geode;
|
||||
geode->addDrawable(new osg::ShapeDrawable(_terrain->_heightField.get()));
|
||||
geode->addDrawable(new osg::ShapeDrawable(hf));
|
||||
|
||||
if (_imagery.valid() && _imagery->_image.valid())
|
||||
{
|
||||
@ -1319,6 +1336,89 @@ osg::Node* DataSet::DestinationTile::createScene()
|
||||
stateset->setTextureAttributeAndModes(0,texture,osg::StateAttribute::ON);
|
||||
}
|
||||
|
||||
// create the skirt.
|
||||
if (false)
|
||||
{
|
||||
|
||||
osg::Vec3 skirtVector(0.0f,0.0f,-0.003f);
|
||||
|
||||
int numColumns = hf->getNumColumns();
|
||||
int numRows = hf->getNumRows();
|
||||
|
||||
int numVerticesInSkirt = 2*(numColumns*2 + numRows*2 - 3);
|
||||
|
||||
osg::Geometry* skirt = new osg::Geometry;
|
||||
osg::Vec3Array& v = *(new osg::Vec3Array(numVerticesInSkirt));
|
||||
osg::Vec3Array& n = *(new osg::Vec3Array(numVerticesInSkirt));
|
||||
osg::Vec2Array& t = *(new osg::Vec2Array(numVerticesInSkirt));
|
||||
|
||||
osg::DrawArrays& skirtDrawArrays = *(new osg::DrawArrays(GL_QUAD_STRIP,0,numVerticesInSkirt));
|
||||
int vi=0;
|
||||
int r,c;
|
||||
// create bottom skirt vertices
|
||||
r=0;
|
||||
float dt_dx = 1.0f/(float)(numColumns-1);
|
||||
float dt_dy = 1.0f/(float)(numRows-1);
|
||||
for(c=0;c<numColumns-1;++c)
|
||||
{
|
||||
v[vi] = hf->getVertex(c,r);
|
||||
n[vi] = hf->getNormal(c,r);
|
||||
t[vi++] = osg::Vec2(c*dt_dx,r*dt_dy);
|
||||
|
||||
v[vi] = hf->getVertex(c,r)+skirtVector;
|
||||
n[vi] = hf->getNormal(c,r);
|
||||
t[vi++] = osg::Vec2(c*dt_dx,r*dt_dy);
|
||||
|
||||
}
|
||||
// create right skirt vertices
|
||||
c=numColumns-1;
|
||||
for(r=0;r<numRows-1;++r)
|
||||
{
|
||||
v[vi] = hf->getVertex(c,r);
|
||||
n[vi] = hf->getNormal(c,r);
|
||||
t[vi++] = osg::Vec2(c*dt_dx,r*dt_dy);
|
||||
|
||||
v[vi] = hf->getVertex(c,r)+skirtVector;
|
||||
n[vi] = hf->getNormal(c,r);
|
||||
t[vi++] = osg::Vec2(c*dt_dx,r*dt_dy);
|
||||
}
|
||||
// create top skirt vertices
|
||||
r=numRows-1;
|
||||
for(c=numColumns-1;c>0;--c)
|
||||
{
|
||||
v[vi] = hf->getVertex(c,r);
|
||||
n[vi] = hf->getNormal(c,r);
|
||||
t[vi++] = osg::Vec2(c*dt_dx,r*dt_dy);
|
||||
|
||||
v[vi] = hf->getVertex(c,r)+skirtVector;
|
||||
n[vi] = hf->getNormal(c,r);
|
||||
t[vi++] = osg::Vec2(c*dt_dx,r*dt_dy);
|
||||
}
|
||||
// create left skirt vertices
|
||||
c=0;
|
||||
for(r=numRows-1;r>=0;--r)
|
||||
{
|
||||
v[vi] = hf->getVertex(c,r);
|
||||
n[vi] = hf->getNormal(c,r);
|
||||
t[vi++] = osg::Vec2(c*dt_dx,r*dt_dy);
|
||||
|
||||
v[vi] = hf->getVertex(c,r)+skirtVector;
|
||||
n[vi] = hf->getNormal(c,r);
|
||||
t[vi++] = osg::Vec2(c*dt_dx,r*dt_dy);
|
||||
}
|
||||
|
||||
// pass arrays to Geometry
|
||||
skirt->setVertexArray(&v);
|
||||
skirt->setNormalArray(&n);
|
||||
skirt->setNormalBinding(osg::Geometry::BIND_PER_VERTEX);
|
||||
skirt->setTexCoordArray(0,&t);
|
||||
|
||||
skirt->addPrimitiveSet(&skirtDrawArrays);
|
||||
|
||||
geode->addDrawable(skirt);
|
||||
|
||||
}
|
||||
|
||||
return geode;
|
||||
}
|
||||
else
|
||||
@ -1375,6 +1475,24 @@ void DataSet::DestinationTile::addRequiredResolutions(CompositeSource* sourceGra
|
||||
}
|
||||
}
|
||||
|
||||
void DataSet::CompositeDestination::computeNeighboursFromQuadMap()
|
||||
{
|
||||
// handle leaves
|
||||
for(TileList::iterator titr=_tiles.begin();
|
||||
titr!=_tiles.end();
|
||||
++titr)
|
||||
{
|
||||
(*titr)->computeNeighboursFromQuadMap();
|
||||
}
|
||||
|
||||
// handle chilren
|
||||
for(ChildList::iterator citr=_children.begin();
|
||||
citr!=_children.end();
|
||||
++citr)
|
||||
{
|
||||
(*citr)->computeNeighboursFromQuadMap();
|
||||
}
|
||||
}
|
||||
|
||||
void DataSet::CompositeDestination::addRequiredResolutions(CompositeSource* sourceGraph)
|
||||
{
|
||||
@ -1617,6 +1735,7 @@ DataSet::CompositeDestination* DataSet::createDestinationGraph(osgTerrain::Coord
|
||||
|
||||
DestinationTile* tile = new DestinationTile;
|
||||
tile->_name = os.str();
|
||||
tile->_dataSet = this;
|
||||
tile->_cs = cs;
|
||||
tile->_extents = extents;
|
||||
tile->_level = currentLevel;
|
||||
@ -1627,6 +1746,8 @@ DataSet::CompositeDestination* DataSet::createDestinationGraph(osgTerrain::Coord
|
||||
tile->computeMaximumSourceResolution(_sourceGraph.get());
|
||||
tile->allocate();
|
||||
|
||||
insertTileToQuadMap(tile);
|
||||
|
||||
if (currentLevel>=maxNumLevels-1)
|
||||
{
|
||||
// bottom level can't divide any further.
|
||||
@ -1801,9 +1922,8 @@ void DataSet::computeDestinationGraphFromSources(unsigned int numLevels)
|
||||
0,
|
||||
numLevels);
|
||||
|
||||
|
||||
|
||||
|
||||
// now traverse the destination graph to build neighbours.
|
||||
_destinationGraph->computeNeighboursFromQuadMap();
|
||||
|
||||
}
|
||||
|
||||
@ -1913,7 +2033,7 @@ void DataSet::updateSourcesForDestinationGraphNeeds()
|
||||
for(CompositeSource::source_iterator itr(_sourceGraph.get());itr.valid();++itr)
|
||||
{
|
||||
Source* source = itr->get();
|
||||
//source->buildOverviews();
|
||||
source->buildOverviews();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -633,6 +633,8 @@ class DataSet : public osg::Referenced
|
||||
|
||||
|
||||
DestinationTile();
|
||||
|
||||
void computeNeighboursFromQuadMap();
|
||||
|
||||
void setNeighbours(DestinationTile* left, DestinationTile* left_below,
|
||||
DestinationTile* below, DestinationTile* below_right,
|
||||
@ -675,6 +677,8 @@ class DataSet : public osg::Referenced
|
||||
|
||||
std::string _name;
|
||||
|
||||
DataSet* _dataSet;
|
||||
|
||||
osg::ref_ptr<DestinationData> _imagery;
|
||||
osg::ref_ptr<DestinationData> _terrain;
|
||||
osg::ref_ptr<DestinationData> _models;
|
||||
@ -712,6 +716,8 @@ class DataSet : public osg::Referenced
|
||||
_type(GROUP),
|
||||
_maxVisibleDistance(FLT_MAX) {}
|
||||
|
||||
void computeNeighboursFromQuadMap();
|
||||
|
||||
void addRequiredResolutions(CompositeSource* sourceGraph);
|
||||
|
||||
void readFrom(CompositeSource* sourceGraph);
|
||||
@ -730,6 +736,29 @@ class DataSet : public osg::Referenced
|
||||
|
||||
};
|
||||
|
||||
|
||||
typedef std::map<unsigned int,DestinationTile*> Column;
|
||||
typedef std::map<unsigned int,Column> Level;
|
||||
typedef std::map<unsigned int,Level> QuadMap;
|
||||
|
||||
void insertTileToQuadMap(DestinationTile* tile)
|
||||
{
|
||||
_quadMap[tile->_level][tile->_X][tile->_Y] = tile;
|
||||
}
|
||||
|
||||
DestinationTile* getTile(unsigned int level,unsigned int X, unsigned int Y)
|
||||
{
|
||||
QuadMap::iterator levelItr = _quadMap.find(level);
|
||||
if (levelItr==_quadMap.end()) return 0;
|
||||
|
||||
Level::iterator columnItr = levelItr->second.find(X);
|
||||
if (columnItr==levelItr->second.end()) return 0;
|
||||
|
||||
Column::iterator rowItr = columnItr->second.find(Y);
|
||||
if (rowItr==columnItr->second.end()) return 0;
|
||||
else return rowItr->second;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
|
||||
@ -777,6 +806,8 @@ class DataSet : public osg::Referenced
|
||||
osg::ref_ptr<CompositeSource> _sourceGraph;
|
||||
|
||||
osg::ref_ptr<CompositeDestination> _destinationGraph;
|
||||
|
||||
QuadMap _quadMap;
|
||||
|
||||
osg::ref_ptr<osgTerrain::CoordinateSystem> _coordinateSystem;
|
||||
osg::Matrixd _geoTransform;
|
||||
|
@ -423,7 +423,9 @@ class SG_EXPORT HeightField : public Shape
|
||||
_rows(0),
|
||||
_origin(0.0f,0.0f,0.0f),
|
||||
_dx(1.0f),
|
||||
_dy(1.0f) {}
|
||||
_dy(1.0f),
|
||||
_skirtHeight(0.0f),
|
||||
_borderWidth(0) {}
|
||||
|
||||
HeightField(const HeightField& mesh,const CopyOp& copyop=CopyOp::SHALLOW_COPY):
|
||||
Shape(mesh,copyop),
|
||||
@ -432,6 +434,8 @@ class SG_EXPORT HeightField : public Shape
|
||||
_origin(mesh._origin),
|
||||
_dx(mesh._dx),
|
||||
_dy(mesh._dy),
|
||||
_skirtHeight(mesh._skirtHeight),
|
||||
_borderWidth(mesh._borderWidth),
|
||||
_heights(mesh._heights) {}
|
||||
|
||||
META_Shape(osg, HeightField)
|
||||
@ -455,6 +459,28 @@ class SG_EXPORT HeightField : public Shape
|
||||
inline void setYInterval(float dy) { _dy = dy; }
|
||||
inline float getYInterval() const { return _dy; }
|
||||
|
||||
|
||||
/** Set the height of the skirt to render around the edge of HeightField.
|
||||
* The skirt is used as a means of disguising edge boundaries between adjacent HeightField, particular
|
||||
* of ones with different resolutions.*/
|
||||
void setSkirtHeight(float skirtHeight) { _skirtHeight = skirtHeight; }
|
||||
|
||||
/** Get the height of the skirt to render around the edge of HeightField.*/
|
||||
float getSkirtHeight() const { return _skirtHeight; }
|
||||
|
||||
/** Set the width in number of cells in from the edge that the height field should be rendered from.
|
||||
* This exists to allow gradient and curvature continutity to be maintained between adjacent HeightField, where
|
||||
* the border cells will overlap adjacent HeightField.*/
|
||||
void setBorderWidth(unsigned int borderWidth) { _borderWidth = borderWidth; }
|
||||
|
||||
/** Get the width in number of cells in from the edge that the height field should be rendered from.*/
|
||||
unsigned int getBorderWidth() const { return _borderWidth; }
|
||||
|
||||
inline void setRotation(const Quat& quat) { _rotation = quat; }
|
||||
inline const Quat& getRotation() const { return _rotation; }
|
||||
inline Matrix getRotationMatrix() const { return Matrix(_rotation); }
|
||||
inline bool zeroRotation() const { return _rotation.zeroRotation(); }
|
||||
|
||||
|
||||
inline void setHeight(unsigned int c,unsigned int r,float value)
|
||||
{
|
||||
@ -474,25 +500,31 @@ class SG_EXPORT HeightField : public Shape
|
||||
HeightList& getHeightList() { return _heights; }
|
||||
const HeightList& getHeightList() const { return _heights; }
|
||||
|
||||
inline Vec3 getVertex(unsigned int c,unsigned int r) const
|
||||
{
|
||||
return Vec3(_origin.x()+getXInterval()*(float)c,
|
||||
_origin.y()+getYInterval()*(float)r,
|
||||
_origin.z()+_heights[c+r*_columns]);
|
||||
}
|
||||
|
||||
Vec3 getNormal(unsigned int c,unsigned int r) const;
|
||||
|
||||
inline void setRotation(const Quat& quat) { _rotation = quat; }
|
||||
inline const Quat& getRotation() const { return _rotation; }
|
||||
inline Matrix getRotationMatrix() const { return Matrix(_rotation); }
|
||||
inline bool zeroRotation() const { return _rotation.zeroRotation(); }
|
||||
|
||||
protected:
|
||||
|
||||
~HeightField() {}
|
||||
|
||||
unsigned int _columns,_rows;
|
||||
unsigned int _columns,_rows;
|
||||
|
||||
osg::Vec3 _origin;
|
||||
float _dx;
|
||||
float _dy;
|
||||
osg::Vec3 _origin;
|
||||
float _dx;
|
||||
float _dy;
|
||||
|
||||
Quat _rotation;
|
||||
HeightList _heights;
|
||||
float _skirtHeight;
|
||||
unsigned int _borderWidth;
|
||||
|
||||
Quat _rotation;
|
||||
HeightList _heights;
|
||||
|
||||
};
|
||||
|
||||
typedef HeightField Grid;
|
||||
|
@ -56,6 +56,11 @@ class Vec3
|
||||
_v[0]=x; _v[1]=y; _v[2]=z;
|
||||
}
|
||||
|
||||
inline void set( const Vec3& rhs)
|
||||
{
|
||||
_v[0]=rhs._v[0]; _v[1]=rhs._v[1]; _v[2]=rhs._v[2];
|
||||
}
|
||||
|
||||
inline float& operator [] (int i) { return _v[i]; }
|
||||
inline float operator [] (int i) const { return _v[i]; }
|
||||
|
||||
|
@ -564,6 +564,7 @@ void DrawShapeVisitor::apply(const HeightField& field)
|
||||
glPushMatrix();
|
||||
|
||||
glTranslatef(field.getOrigin().x(),field.getOrigin().y(),field.getOrigin().z());
|
||||
|
||||
|
||||
if (!field.zeroRotation())
|
||||
{
|
||||
@ -578,21 +579,102 @@ void DrawShapeVisitor::apply(const HeightField& field)
|
||||
float dv = 1.0f/((float)field.getNumRows()-1.0f);
|
||||
|
||||
float vBase = 0.0f;
|
||||
|
||||
Vec3 vertTop;
|
||||
Vec3 normTop;
|
||||
|
||||
Vec3 vertBase;
|
||||
Vec3 normBase;
|
||||
|
||||
if (field.getSkirtHeight()!=0.0f)
|
||||
{
|
||||
glBegin(GL_QUAD_STRIP);
|
||||
|
||||
float u = 0.0f;
|
||||
|
||||
// draw bottom skirt
|
||||
unsigned int col;
|
||||
vertTop.y() = 0.0f;
|
||||
for(col=0;col<field.getNumColumns();++col,u+=du)
|
||||
{
|
||||
vertTop.x() = dx*(float)col;
|
||||
vertTop.z() = field.getHeight(col,0);
|
||||
normTop.set(field.getNormal(col,0));
|
||||
|
||||
glTexCoord2f(u,0.0f);
|
||||
glNormal3fv(normTop.ptr());
|
||||
|
||||
glVertex3fv(vertTop.ptr());
|
||||
|
||||
vertTop.z()-=field.getSkirtHeight();
|
||||
|
||||
glVertex3fv(vertTop.ptr());
|
||||
}
|
||||
|
||||
glEnd();
|
||||
|
||||
// draw top skirt
|
||||
glBegin(GL_QUAD_STRIP);
|
||||
|
||||
unsigned int row = field.getNumRows()-1;
|
||||
|
||||
u = 0.0f;
|
||||
vertTop.y() = dy*(float)(row);
|
||||
for(col=0;col<field.getNumColumns();++col,u+=du)
|
||||
{
|
||||
vertTop.x() = dx*(float)col;
|
||||
vertTop.z() = field.getHeight(col,row);
|
||||
normTop.set(field.getNormal(col,row));
|
||||
|
||||
glTexCoord2f(u,1.0f);
|
||||
glNormal3fv(normTop.ptr());
|
||||
|
||||
glVertex3f(vertTop.x(),vertTop.y(),vertTop.z()-field.getSkirtHeight());
|
||||
|
||||
//vertTop.z()-=field.getSkirtHeight();
|
||||
|
||||
glVertex3fv(vertTop.ptr());
|
||||
}
|
||||
|
||||
glEnd();
|
||||
}
|
||||
|
||||
|
||||
|
||||
for(unsigned int row=0;row<field.getNumRows()-1;++row,vBase+=dv)
|
||||
{
|
||||
|
||||
float vTop = vBase+dv;
|
||||
float u = 0.0f;
|
||||
|
||||
|
||||
glBegin(GL_QUAD_STRIP);
|
||||
|
||||
// draw skirt at begining if required.
|
||||
if (field.getSkirtHeight()!=0.0f)
|
||||
{
|
||||
vertTop.set(0.0f,dy*(float)row+dy,field.getHeight(0,row+1)-field.getSkirtHeight());
|
||||
normTop.set(field.getNormal(0,row+1));
|
||||
|
||||
vertBase.set(0.0f,dy*(float)row,field.getHeight(0,row)-field.getSkirtHeight());
|
||||
normBase.set(field.getNormal(0,row));
|
||||
|
||||
glTexCoord2f(u,vTop);
|
||||
glNormal3fv(normTop.ptr());
|
||||
glVertex3fv(vertTop.ptr());
|
||||
|
||||
glTexCoord2f(u,vBase);
|
||||
glNormal3fv(normBase.ptr());
|
||||
glVertex3fv(vertBase.ptr());
|
||||
}
|
||||
|
||||
for(unsigned int col=0;col<field.getNumColumns();++col,u+=du)
|
||||
{
|
||||
Vec3 vertTop(dx*(float)col,dy*(float)row+dy,field.getHeight(col,row+1));
|
||||
Vec3 normTop(field.getNormal(col,row+1));
|
||||
vertTop.set(dx*(float)col,dy*(float)row+dy,field.getHeight(col,row+1));
|
||||
normTop.set(field.getNormal(col,row+1));
|
||||
|
||||
Vec3 vertBase(dx*(float)col,dy*(float)row,field.getHeight(col,row));
|
||||
Vec3 normBase(field.getNormal(col,row));
|
||||
vertBase.set(dx*(float)col,dy*(float)row,field.getHeight(col,row));
|
||||
normBase.set(field.getNormal(col,row));
|
||||
|
||||
glTexCoord2f(u,vTop);
|
||||
glNormal3fv(normTop.ptr());
|
||||
@ -604,6 +686,22 @@ void DrawShapeVisitor::apply(const HeightField& field)
|
||||
|
||||
}
|
||||
|
||||
// draw skirt at end if required.
|
||||
if (field.getSkirtHeight()!=0.0f)
|
||||
{
|
||||
|
||||
vertBase.z()-=field.getSkirtHeight();
|
||||
vertTop.z()-=field.getSkirtHeight();
|
||||
|
||||
glTexCoord2f(u,vTop);
|
||||
glNormal3fv(normTop.ptr());
|
||||
glVertex3fv(vertTop.ptr());
|
||||
|
||||
glTexCoord2f(u,vBase);
|
||||
glNormal3fv(normBase.ptr());
|
||||
glVertex3fv(vertBase.ptr());
|
||||
}
|
||||
|
||||
glEnd();
|
||||
}
|
||||
|
||||
|
@ -358,6 +358,24 @@ bool HeightField_readLocalData(Object& obj, Input& fr)
|
||||
iteratorAdvanced = true;
|
||||
}
|
||||
|
||||
if (fr.matchSequence("SkirtHeight %f"))
|
||||
{
|
||||
float height;
|
||||
fr[1].getFloat(height);
|
||||
heightfield.setSkirtHeight(height);
|
||||
fr+=2;
|
||||
iteratorAdvanced = true;
|
||||
}
|
||||
|
||||
if (fr.matchSequence("BorderWidth %i"))
|
||||
{
|
||||
unsigned int width;
|
||||
fr[1].getUInt(width);
|
||||
heightfield.setBorderWidth(width);
|
||||
fr+=2;
|
||||
iteratorAdvanced = true;
|
||||
}
|
||||
|
||||
if (fr.matchSequence("Rotation %f %f %f %f"))
|
||||
{
|
||||
osg::Quat rotation;
|
||||
@ -426,6 +444,8 @@ bool HeightField_writeLocalData(const Object& obj, Output& fw)
|
||||
fw.indent()<<"Origin "<<heightfield.getOrigin().x()<<" "<<heightfield.getOrigin().y()<<" "<<heightfield.getOrigin().z()<<std::endl;
|
||||
fw.indent()<<"XInterval "<<heightfield.getXInterval()<<std::endl;
|
||||
fw.indent()<<"YInterval "<<heightfield.getYInterval()<<std::endl;
|
||||
fw.indent()<<"SkirtHeight "<<heightfield.getSkirtHeight()<<std::endl;
|
||||
fw.indent()<<"BorderWidth "<<heightfield.getBorderWidth()<<std::endl;
|
||||
fw.indent()<<"Rotation "<<heightfield.getRotation()<<std::endl;
|
||||
fw.precision(prec);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user