Improved 3D clouds

1) Impostors
2) One drawable per cloud rather than a single drawable shared by all clouds.
This commit is contained in:
Stuart Buchanan
2011-12-17 13:02:54 +00:00
parent b20a49b018
commit 965c8ccfd2
7 changed files with 294 additions and 145 deletions

View File

@@ -51,79 +51,94 @@ void CloudShaderGeometry::drawImplementation(RenderInfo& renderInfo) const
if (!_cloudsprites.size()) return;
osg::State& state = *renderInfo.getState();
unsigned int contextID = state.getContextID();
SortData& sortData = _sortData[contextID];
int frameNumber = state.getFrameStamp()->getFrameNumber();
if (!sortData.spriteIdx)
sortData.spriteIdx = new SortData::SortItemList;
if (sortData.spriteIdx->size() < _cloudsprites.size()) {
for (unsigned i = sortData.spriteIdx->size(); i < (unsigned)_cloudsprites.size(); ++i)
sortData.spriteIdx->push_back(SortData::SortItem(i, 0.0f));
sortData.frameSorted = frameNumber - (sortData.skip_limit + 1);
}
unsigned int contextID = state.getContextID();
SortData& sortData = _sortData[contextID];
Geometry* g = _geometry->asGeometry();
// If the cloud is already sorted, then it is likely to still be sorted.
// Therefore we can avoid re-sorting it for a period. If it is still
// sorted after that period, then we can wait for a longer period before
// checking again. In this way, only clouds that are changing regularly
// are sorted.
if (frameNumber - sortData.skip_limit >= sortData.frameSorted) {
// are sorted.
osg::Vec3Array* v = dynamic_cast<osg::Vec3Array*>(g->getVertexArray());
if ((v->size() > 4) &&
(frameNumber - sortData.skip_limit >= sortData.frameSorted)) {
Matrix mvp = state.getModelViewMatrix() * state.getProjectionMatrix();
for (SortData::SortItemList::iterator itr = sortData.spriteIdx->begin(),
end = sortData.spriteIdx->end();
itr != end;
++itr) {
Vec4f projPos
= Vec4f(toOsg(_cloudsprites[itr->idx].position), 1.0f) * mvp;
itr->depth = projPos.z() / projPos.w();
osg::Vec4Array* c = dynamic_cast<osg::Vec4Array*>(g->getColorArray());
osg::Vec2Array* t = dynamic_cast<osg::Vec2Array*>(g->getTexCoordArray(0));
Vec3f av[4];
Vec4f ac[4];
Vec2f at[4];
// Perform a single pass bubble sort of the array,
// keeping track of whether we've had to make any changes
bool sorted = true;
for (unsigned int i = 4; i < v->size(); i = i + 4) {
// The position of the sprite is stored in the colour
// array, with the exception of the w() coordinate
// which is the z-scaling parameter.
Vec4f a = (*c)[i-4];
Vec4f aPos = Vec4f(a.x(), a.y(), a.z(), 1.0f) * mvp;
Vec4f b = (*c)[i];
Vec4f bPos = Vec4f(b.x(), b.y(), b.z(), 1.0f) * mvp;
if ((aPos.z()/aPos.w()) < (bPos.z()/bPos.w() - 0.0001)) {
// a is non-trivially closer than b, so should be rendered
// later. Swap them around
for (int j = 0; j < 4; j++) {
av[j] = (*v)[i+j-4];
ac[j] = (*c)[i+j-4];
at[j] = (*t)[i+j-4];
(*v)[i+j -4] = (*v)[i+j];
(*c)[i+j -4] = (*c)[i+j];
(*t)[i+j -4] = (*t)[i+j];
(*v)[i+j] = av[j];
(*c)[i+j] = ac[j];
(*t)[i+j] = at[j];
}
// Indicate that the arrays were not sorted
// so we should check them next iteration
sorted = false;
}
}
// Already sorted?
if (std::adjacent_find(sortData.spriteIdx->rbegin(),
sortData.spriteIdx->rend(), SpriteComp())
== sortData.spriteIdx->rend()) {
if (sorted) {
// This cloud is sorted, so no need to re-sort.
sortData.skip_limit = sortData.skip_limit * 2;
if (sortData.skip_limit > 30) {
// Jitter the skip frames to avoid synchronized sorts
// which will cause periodic frame-rate drops
sortData.skip_limit += sg_random() * 10;
}
if (sortData.skip_limit > 128) {
// Maximum of every 128 frames (2 - 4 seconds)
sortData.skip_limit = 128 + sg_random() * 10;
if (sortData.skip_limit > 500) {
// Maximum of every 500 frames (10-20 seconds)
sortData.skip_limit = 500 + sg_random() * 10;
}
} else {
std::sort(sortData.spriteIdx->begin(), sortData.spriteIdx->end(),
SpriteComp());
sortData.skip_limit = 1;
}
sortData.frameSorted = frameNumber;
}
const Extensions* extensions = getExtensions(state.getContextID(),true);
for(SortData::SortItemList::const_iterator itr = sortData.spriteIdx->begin(),
end = sortData.spriteIdx->end();
itr != end;
++itr) {
const CloudSprite& t = _cloudsprites[itr->idx];
GLfloat ua1[3] = { (GLfloat) t.texture_index_x/varieties_x,
(GLfloat) t.texture_index_y/varieties_y,
(GLfloat) t.width };
GLfloat ua2[3] = { (GLfloat) t.height,
(GLfloat) shade_factor,
(GLfloat) cloud_height };
GLfloat ua3[3] = { (GLfloat) bottom_factor,
(GLfloat) middle_factor,
(GLfloat) top_factor };
extensions->glVertexAttrib3fv(USR_ATTR_1, ua1 );
extensions->glVertexAttrib3fv(USR_ATTR_2, ua2 );
extensions->glVertexAttrib3fv(USR_ATTR_3, ua3 );
glColor4f(t.position.x(), t.position.y(), t.position.z(), zscale);
_geometry->draw(renderInfo);
}
GLfloat ua1[3] = { (GLfloat) 1.0f,
(GLfloat) shade_factor,
(GLfloat) cloud_height };
GLfloat ua2[3] = { (GLfloat) bottom_factor,
(GLfloat) middle_factor,
(GLfloat) top_factor };
extensions->glVertexAttrib3fv(USR_ATTR_1, ua1 );
extensions->glVertexAttrib3fv(USR_ATTR_2, ua2 );
_geometry->draw(renderInfo);
}
void CloudShaderGeometry::addSprite(const SGVec3f& p, int tx, int ty,
@@ -145,6 +160,73 @@ void CloudShaderGeometry::addSprite(const SGVec3f& p, int tx, int ty,
_cloudsprites.push_back(CloudSprite(p, tx, ty, w, h));
}
void CloudShaderGeometry::generateGeometry()
{
// Generate a set of geometries as a QuadStrip based on the list of sprites
int numsprites = _cloudsprites.size();
// Create front and back polygons so we don't need to screw around
// with two-sided lighting in the shader.
osg::Vec3Array& v = *(new osg::Vec3Array(4 * numsprites));
osg::Vec4Array& c = *(new osg::Vec4Array(4 * numsprites));
osg::Vec2Array& t = *(new osg::Vec2Array(4 * numsprites));
int idx = 0;
for (CloudShaderGeometry::CloudSpriteList::iterator iter = _cloudsprites.begin();
iter != _cloudsprites.end();
++iter)
{
float cw = 0.5f * iter->width;
float ch = 0.5f * iter->height;
// Create the vertices
v[4*idx ].set(0.0f, -cw, -ch);
v[4*idx+1].set(0.0f, cw, -ch);
v[4*idx+2].set(0.0f, cw, ch);
v[4*idx+3].set(0.0f, -cw, ch);
// Set the texture coords for each vertex
// from the texture index, and the number
// of textures in the image
int x = iter->texture_index_x;
int y = iter->texture_index_y;
t[4*idx ].set( (float) x / varieties_x, (float) y / varieties_y);
t[4*idx+1].set( (float) (x + 1) / varieties_x, (float) y / varieties_y);
t[4*idx+2].set( (float) (x + 1) / varieties_x, (float) (y + 1) / varieties_y);
t[4*idx+3].set( (float) x / varieties_x, (float) (y + 1) / varieties_y);
// The color isn't actually use in lighting, but instead to indicate the center of rotation
c[4*idx ].set(iter->position.x(), iter->position.y(), iter->position.z(), zscale);
c[4*idx+1].set(iter->position.x(), iter->position.y(), iter->position.z(), zscale);
c[4*idx+2].set(iter->position.x(), iter->position.y(), iter->position.z(), zscale);
c[4*idx+3].set(iter->position.x(), iter->position.y(), iter->position.z(), zscale);
idx++;
}
//Quads now created, add it to the geometry.
osg::Geometry* geom = new osg::Geometry;
geom->setVertexArray(&v);
geom->setTexCoordArray(0, &t);
// The normal isn't actually use in lighting, so we simply bind overall.
osg::Vec3Array& n = *(new osg::Vec3Array(4));
n[0].set(1.0f, -1.0f, -1.0f);
n[1].set(1.0f, 1.0f, -1.0f);
n[2].set(1.0f, 1.0f, 1.0f);
n[3].set(1.0f, -1.0f, 1.0f);
geom->setNormalArray(&n);
geom->setNormalBinding(Geometry::BIND_OVERALL);
geom->setColorArray(&c);
geom->setColorBinding(Geometry::BIND_PER_VERTEX);
geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,numsprites*4));
_geometry = geom;
}
bool CloudShaderGeometry_readLocalData(Object& obj, Input& fr)
{
bool iteratorAdvanced = false;
@@ -181,6 +263,7 @@ bool CloudShaderGeometry_readLocalData(Object& obj, Input& fr)
++fr;
}
}
geom.generateGeometry();
}
return iteratorAdvanced;
}

View File

@@ -100,12 +100,9 @@ class CloudShaderGeometry : public osg::Drawable
return _bbox;
}
void setGeometry(osg::Drawable* geometry)
{
_geometry = geometry;
}
void addSprite(const SGVec3f& p, int tx, int ty, float w, float h, float cull);
void generateGeometry();
void rebuildGeometry();
osg::ref_ptr<osg::Drawable> _geometry;

View File

@@ -28,6 +28,7 @@
#include <osg/Texture2D>
#include <osg/PositionAttitudeTransform>
#include <osg/Vec4f>
#include <osgSim/Impostor>
#include <simgear/compiler.h>
@@ -68,9 +69,15 @@ float SGCloudField::fieldSize = 50000.0f;
double SGCloudField::timer_dt = 0.0;
float SGCloudField::view_distance = 20000.0f;
bool SGCloudField::wrap = true;
float SGCloudField::RADIUS_LEVEL_1 = 5000.0f;
float SGCloudField::RADIUS_LEVEL_2 = 2000.0f;
float SGCloudField::MAX_CLOUD_DEPTH = 2000.0f;
bool SGCloudField::use_impostors = true;
float SGCloudField::lod1_range = 10000.0f;
float SGCloudField::lod2_range = 5000.0f;
float SGCloudField::impostor_distance = 10000.0f;
int impostorcount = 0;
int lodcount = 0;
int cloudcount = 0;
SGVec3f SGCloudField::view_vec, SGCloudField::view_X, SGCloudField::view_Y;
@@ -165,6 +172,9 @@ SGCloudField::SGCloudField() :
field_transform->addChild(altitude_transform.get());
placed_root = new osg::Group();
altitude_transform->addChild(placed_root);
impostorcount = 0;
lodcount = 0;
cloudcount = 0;
}
SGCloudField::~SGCloudField() {
@@ -182,12 +192,12 @@ void SGCloudField::clear(void) {
cloud_hash.clear();
}
void SGCloudField::applyVisRange(void)
void SGCloudField::applyVisAndLoDRange(void)
{
for (unsigned int i = 0; i < placed_root->getNumChildren(); i++) {
osg::ref_ptr<osg::LOD> lodnode1 = (osg::LOD*) placed_root->getChild(i);
for (unsigned int j = 0; j < lodnode1->getNumChildren(); j++) {
lodnode1->setRange(j, 0.0f, view_distance + RADIUS_LEVEL_1 + RADIUS_LEVEL_2 + MAX_CLOUD_DEPTH);
lodnode1->setRange(j, 0.0f, lod1_range + view_distance + MAX_CLOUD_DEPTH);
osg::ref_ptr<osg::LOD> lodnode2 = (osg::LOD*) lodnode1->getChild(j);
for (unsigned int k = 0; k < lodnode2->getNumChildren(); k++) {
lodnode2->setRange(k, 0.0f, view_distance + MAX_CLOUD_DEPTH);
@@ -217,15 +227,19 @@ void SGCloudField::removeCloudFromTree(osg::ref_ptr<osg::PositionAttitudeTransfo
{
osg::ref_ptr<osg::Group> lodnode = transform->getParent(0);
lodnode->removeChild(transform);
cloudcount--;
// Clean up the LOD nodes if required
if (lodnode->getNumChildren() == 0) {
osg::ref_ptr<osg::Group> lodnode1 = lodnode->getParent(0);
osg::ref_ptr<osgSim::Impostor> impostornode = (osgSim::Impostor*) lodnode1->getParent(0);
lodnode1->removeChild(lodnode);
if (lodnode1->getNumChildren() == 0) {
placed_root->removeChild(lodnode1);
lodcount--;
if (lodnode1->getNumChildren() == 0) {
impostornode->removeChild(lodnode1);
placed_root->removeChild(impostornode);
impostorcount--;
}
}
}
@@ -291,39 +305,55 @@ void SGCloudField::addCloudToTree(osg::ref_ptr<osg::PositionAttitudeTransform> t
bool found = false;
osg::ref_ptr<osg::LOD> lodnode1;
osg::ref_ptr<osg::LOD> lodnode;
osg::ref_ptr<osgSim::Impostor> impostornode;
for (unsigned int i = 0; (!found) && (i < placed_root->getNumChildren()); i++) {
lodnode1 = (osg::LOD*) placed_root->getChild(i);
if ((lodnode1->getCenter() - pos).length2() < RADIUS_LEVEL_1*RADIUS_LEVEL_1) {
// New cloud is within RADIUS_LEVEL_1 of the center of the LOD node.
found = true;
}
if ((lodnode1->getCenter() - pos).length2() < lod1_range*lod1_range) {
// New cloud is within RADIUS_LEVEL_1 of the center of the LOD node.
found = true;
}
}
if (!found) {
lodnode1 = new osg::LOD();
placed_root->addChild(lodnode1.get());
if (use_impostors) {
impostornode = new osgSim::Impostor();
impostornode->setImpostorThreshold(impostor_distance);
//impostornode->setImpostorThresholdToBound();
//impostornode->setCenter(pos);
placed_root->addChild(impostornode.get());
lodnode1 = (osg::ref_ptr<osg::LOD>) impostornode;
} else {
lodnode1 = new osg::LOD();
placed_root->addChild(lodnode1.get());
}
impostorcount++;
}
// Now check if there is a second level LOD node at an appropriate distance
found = false;
for (unsigned int j = 0; (!found) && (j < lodnode1->getNumChildren()); j++) {
for (unsigned int j = 0; (!found) && (j < lodnode1->getNumChildren()); j++) {
lodnode = (osg::LOD*) lodnode1->getChild(j);
if ((lodnode->getCenter() - pos).length2() < RADIUS_LEVEL_2*RADIUS_LEVEL_2) {
if ((lodnode->getCenter() - pos).length2() < lod2_range*lod2_range) {
// We've found the right leaf LOD node
found = true;
}
}
if (!found) {
// No suitable leave node was found, so we need to add one.
// No suitable leaf node was found, so we need to add one.
lodnode = new osg::LOD();
lodnode1->addChild(lodnode, 0.0f, view_distance + RADIUS_LEVEL_1 + RADIUS_LEVEL_2 + MAX_CLOUD_DEPTH);
}
lodnode1->addChild(lodnode, 0.0f, lod1_range + view_distance + MAX_CLOUD_DEPTH);
lodcount++;
}
transform->setPosition(pos);
lodnode->addChild(transform.get(), 0.0f, view_distance + MAX_CLOUD_DEPTH);
lodnode->addChild(transform.get(), 0.0f, view_distance);
cloudcount++;
SG_LOG(SG_ENVIRONMENT, SG_DEBUG, "Impostors: " << impostorcount <<
" LoD: " << lodcount <<
" Clouds: " << cloudcount);
lodnode->dirtyBound();
lodnode1->dirtyBound();

View File

@@ -72,16 +72,12 @@ private:
float Rnd(float);
// Radius of the LoD nodes for the dynamic quadtrees.
static float RADIUS_LEVEL_1;
static float RADIUS_LEVEL_2;
// Theoretical maximum cloud depth, used for fudging the LoD
// ranges to ensure that clouds become visible at maximum range
static float MAX_CLOUD_DEPTH;
// this is a relative position only, with that we can move all clouds at once
SGVec3f relative_position;
// this is a relative position only, with that we can move all clouds at once
SGVec3f relative_position;
osg::ref_ptr<osg::Group> field_root;
osg::ref_ptr<osg::Group> placed_root;
@@ -150,6 +146,10 @@ public:
static SGVec3f view_vec, view_X, view_Y;
static float view_distance;
static float impostor_distance;
static float lod1_range;
static float lod2_range;
static bool use_impostors;
static double timer_dt;
static float fieldSize;
static bool wrap;
@@ -157,9 +157,18 @@ public:
static bool getWrap(void) { return wrap; }
static void setWrap(bool w) { wrap = w; }
static float getImpostorDistance(void) { return impostor_distance; }
static void setImpostorDistance(float d) { impostor_distance = d; }
static float getLoD1Range(void) { return lod1_range; }
static void setLoD1Range(int d) { lod1_range = d; }
static float getLoD2Range(void) { return lod2_range; }
static void setLoD2Range(int d) { lod2_range = d; }
static void setUseImpostors(bool b) { use_impostors = b; }
static bool getUseImpostors(void) { return use_impostors; }
static float getVisRange(void) { return view_distance; }
static void setVisRange(float d) { view_distance = d; }
void applyVisRange(void);
void applyVisAndLoDRange(void);
static osg::Fog* getFog()
{

View File

@@ -119,58 +119,11 @@ SGNewCloud::SGNewCloud(const SGPath &texture_root, const SGPropertyNode *cld_def
} else {
effect = iter->second.get();
}
quad = createOrthQuad(min_sprite_width, min_sprite_height,
num_textures_x, num_textures_y);
}
SGNewCloud::~SGNewCloud() {
}
osg::Geometry* SGNewCloud::createOrthQuad(float w, float h, int varieties_x, int varieties_y)
{
// Create front and back polygons so we don't need to screw around
// with two-sided lighting in the shader.
osg::Vec3Array& v = *(new osg::Vec3Array(4));
osg::Vec3Array& n = *(new osg::Vec3Array(4));
osg::Vec2Array& t = *(new osg::Vec2Array(4));
float cw = w*0.5f;
float ch = h*0.5f;
v[0].set(0.0f, -cw, -ch);
v[1].set(0.0f, cw, -ch);
v[2].set(0.0f, cw, ch);
v[3].set(0.0f, -cw, ch);
// The texture coordinate range is not the
// entire coordinate space - as the texture
// has a number of different clouds on it.
float tx = 1.0f/varieties_x;
float ty = 1.0f/varieties_y;
t[0].set(0.0f, 0.0f);
t[1].set( tx, 0.0f);
t[2].set( tx, ty);
t[3].set(0.0f, ty);
// The normal isn't actually use in lighting.
n[0].set(1.0f, -1.0f, -1.0f);
n[1].set(1.0f, 1.0f, -1.0f);
n[2].set(1.0f, 1.0f, 1.0f);
n[3].set(1.0f, -1.0f, 1.0f);
osg::Geometry *geom = new osg::Geometry;
geom->setVertexArray(&v);
geom->setTexCoordArray(0, &t);
geom->setNormalArray(&n);
geom->setNormalBinding(Geometry::BIND_PER_VERTEX);
// No color for now; that's used to pass the position.
geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4));
return geom;
}
#if 0
// return a random number between -n/2 and n/2, tending to 0
static float Rnd(float n) {
@@ -240,28 +193,28 @@ osg::ref_ptr<EffectGeode> SGNewCloud::genCloud() {
z = height * cos(elev) * 0.5f + height * 0.5f;
}
// Determine the height and width as scaling factors on the minimum size (used to create the quad).
float sprite_width = 1.0f + sg_random() * (max_sprite_width - min_sprite_width) / min_sprite_width;
float sprite_height = 1.0f + sg_random() * (max_sprite_height - min_sprite_height) / min_sprite_height;
// Determine the height and width
float sprite_width = min_sprite_width + sg_random() * (max_sprite_width - min_sprite_width);
float sprite_height = min_sprite_height + sg_random() * (max_sprite_height - min_sprite_height);
// Sprites are never taller than square.
if (sprite_height * min_sprite_height > sprite_width * min_sprite_width)
if (sprite_height > sprite_width )
{
sprite_height = sprite_width * min_sprite_width / min_sprite_height;
sprite_height = sprite_width;
}
if (i == 0) {
// The center sprite is always maximum size to fill up any holes.
sprite_width = 1.0f + (max_sprite_width - min_sprite_width) / min_sprite_width;
sprite_height = 1.0f + (max_sprite_height - min_sprite_height) / min_sprite_height;
sprite_width = max_sprite_width;
sprite_height = max_sprite_height;
}
// If the center of the sprite is less than half the sprite heightthe sprite will extend
// below the bottom of the cloud and must be shifted upwards. This is particularly important
// for cumulus clouds which have a very well defined base.
if (z < 0.5f * sprite_height * min_sprite_height)
if (z < 0.5f * sprite_height)
{
z = 0.5f * sprite_height * min_sprite_height;
z = 0.5f * sprite_height;
}
// Determine the sprite texture indexes.
@@ -287,7 +240,7 @@ osg::ref_ptr<EffectGeode> SGNewCloud::genCloud() {
cull_distance_squared);
}
sg->setGeometry(quad);
sg->generateGeometry();
geode->addDrawable(sg);
geode->setName("3D cloud");
geode->setEffect(effect.get());

View File

@@ -233,7 +233,43 @@ void SGSky::set_3dCloudVisRange(float vis)
{
SGCloudField::setVisRange(vis);
for ( int i = 0; i < (int)cloud_layers.size(); ++i ) {
cloud_layers[i]->get_layer3D()->applyVisRange();
cloud_layers[i]->get_layer3D()->applyVisAndLoDRange();
}
}
float SGSky::get_3dCloudImpostorDistance() const {
return SGCloudField::getImpostorDistance();
}
void SGSky::set_3dCloudImpostorDistance(float vis)
{
SGCloudField::setImpostorDistance(vis);
for ( int i = 0; i < (int)cloud_layers.size(); ++i ) {
cloud_layers[i]->get_layer3D()->applyVisAndLoDRange();
}
}
float SGSky::get_3dCloudLoD1Range() const {
return SGCloudField::getLoD1Range();
}
void SGSky::set_3dCloudLoD1Range(float vis)
{
SGCloudField::setLoD1Range(vis);
for ( int i = 0; i < (int)cloud_layers.size(); ++i ) {
cloud_layers[i]->get_layer3D()->applyVisAndLoDRange();
}
}
float SGSky::get_3dCloudLoD2Range() const {
return SGCloudField::getLoD2Range();
}
void SGSky::set_3dCloudLoD2Range(float vis)
{
SGCloudField::setLoD2Range(vis);
for ( int i = 0; i < (int)cloud_layers.size(); ++i ) {
cloud_layers[i]->get_layer3D()->applyVisAndLoDRange();
}
}
@@ -246,6 +282,15 @@ void SGSky::set_3dCloudWrap(bool wrap)
SGCloudField::setWrap(wrap);
}
bool SGSky::get_3dCloudUseImpostors() const {
return SGCloudField::getUseImpostors();
}
void SGSky::set_3dCloudUseImpostors(bool imp)
{
SGCloudField::setUseImpostors(imp);
}
void SGSky::texture_path( const string& path ) {
tex_path = SGPath( path );

View File

@@ -437,6 +437,38 @@ public:
*/
virtual void set_3dCloudVisRange(float vis);
/** Get 3D cloud impostor distance*/
virtual float get_3dCloudImpostorDistance() const;
/** Set 3D cloud impostor distance
* @param density 3D cloud impostor distance
*/
virtual void set_3dCloudImpostorDistance(float vis);
/** Get 3D cloud LoD1 Range*/
virtual float get_3dCloudLoD1Range() const;
/** Set 3D cloud LoD1 Range
* @param vis LoD1 Range
*/
virtual void set_3dCloudLoD1Range(float vis);
/** Get 3D cloud LoD2 Range*/
virtual float get_3dCloudLoD2Range() const;
/** Set 3D cloud LoD2 Range
* @param vis LoD2 Range
*/
virtual void set_3dCloudLoD2Range(float vis);
/** Get 3D cloud impostor usage */
virtual bool get_3dCloudUseImpostors() const;
/** Set 3D cloud impostor usage
* @param wrap whether use impostors for 3D clouds
*/
virtual void set_3dCloudUseImpostors(bool imp);
/** Get 3D cloud wrapping */
virtual bool get_3dCloudWrap() const;