panda3d/panda/src/grutil/geoMipTerrain.cxx
2008-10-11 10:00:25 +00:00

588 lines
25 KiB
C++

// Filename: geoMipTerrain.cxx
// Created by: pro-rsoft (29jun07)
// Last updated by: pro-rsoft (25sep08)
//
////////////////////////////////////////////////////////////////////
//
// PANDA 3D SOFTWARE
// Copyright (c) Carnegie Mellon University. All rights reserved.
//
// All use of this software is subject to the terms of the revised BSD
// license. You should have received a copy of this license along
// with this source code in a file named "LICENSE."
//
////////////////////////////////////////////////////////////////////
#include "geoMipTerrain.h"
#include "geomVertexFormat.h"
#include "geomVertexArrayFormat.h"
#include "internalName.h"
#include "geomVertexData.h"
#include "geomVertexWriter.h"
#include "geomTristrips.h"
#include "geomTriangles.h"
#include "geom.h"
#include "geomNode.h"
#include "config_grutil.h"
#include "sceneGraphReducer.h"
#include "collideMask.h"
TypeHandle GeoMipTerrain::_type_handle;
////////////////////////////////////////////////////////////////////
// Function: GeoMipTerrain::generate_block
// Access: Private
// Description: Generates a chunk of terrain based on the level
// specified. As arguments it takes the x and y coords
// of the mipmap to be generated, and the level of
// detail. T-Junctions for neighbor-mipmaps with
// different levels are also taken into account.
////////////////////////////////////////////////////////////////////
NodePath GeoMipTerrain::
generate_block(unsigned short mx,
unsigned short my,
unsigned short level) {
nassertr(mx < (_xsize - 1) / _block_size, NodePath::fail());
nassertr(my < (_ysize - 1) / _block_size, NodePath::fail());
unsigned short center = _block_size / 2;
unsigned int vcounter = 0;
// Create the format
PT(GeomVertexArrayFormat) array = new GeomVertexArrayFormat();
if (_has_color_map) {
array->add_column(InternalName::make("color"), 4,
Geom::NT_float32, Geom::C_color);
}
array->add_column(InternalName::make("vertex"), 3,
Geom::NT_float32, Geom::C_point);
array->add_column(InternalName::make("texcoord"), 2,
Geom::NT_float32, Geom::C_texcoord);
array->add_column(InternalName::make("normal"), 3,
Geom::NT_float32, Geom::C_vector);
PT(GeomVertexFormat) format = new GeomVertexFormat();
format->add_array(array);
// Create vertex data and writers
PT(GeomVertexData) vdata = new GeomVertexData(_root.get_name(),
GeomVertexFormat::register_format(format), Geom::UH_stream);
GeomVertexWriter cwriter;
if (_has_color_map) {
cwriter=GeomVertexWriter(vdata, "color" );
}
GeomVertexWriter vwriter (vdata, "vertex" );
GeomVertexWriter twriter (vdata, "texcoord");
GeomVertexWriter nwriter (vdata, "normal" );
PT(GeomTriangles) prim = new GeomTriangles(Geom::UH_stream);
if (_bruteforce) {
// LOD Level when rendering bruteforce is always 0 (no lod)
level = 0;
}
// Do some calculations with the level
level = min(short(max(_min_level, level)), short(_max_level));
unsigned short reallevel = level;
level = int(pow(2.0, int(level)));
// Confusing note:
// the variable level contains not the actual level as described
// in the GeoMipMapping paper. That is stored in reallevel,
// while the variable level contains 2^reallevel.
// This is the number of vertices at the certain level.
unsigned short lowblocksize = _block_size / level + 1;
for (int x = 0; x <= _block_size; x++) {
for (int y = 0; y <= _block_size; y++) {
if ((x % level) == 0 && (y % level) == 0) {
if (_has_color_map) {
LVecBase4d color = _color_map.get_xel_a(int((mx * _block_size + x)
/ double(_xsize) * _color_map.get_x_size()),
int((my * _block_size + y)
/ double(_ysize) * _color_map.get_y_size()));
cwriter.add_data4f(color.get_x(), color.get_y(),
color.get_z(), color.get_w());
}
vwriter.add_data3f(x - 0.5 * _block_size, y - 0.5 * _block_size,
get_pixel_value(mx, my, x, y));
twriter.add_data2f((mx * _block_size + x) / double(_xsize - 1),
(my * _block_size + y) / double(_ysize - 1));
nwriter.add_data3f(get_normal(mx, my, x, y));
if (x > 0 && y > 0) {
//left border
if (!_bruteforce && x == level && mx > 0 && _levels[mx - 1][my] > reallevel) {
if (y > level && y < _block_size) {
prim->add_vertex(min(max(sfav(y / level, _levels[mx - 1][my], reallevel), 0), lowblocksize - 1));
prim->add_vertex(vcounter - 1);
prim->add_vertex(vcounter);
prim->close_primitive();
}
if (f_part((y / level) / float(pow(2.0, int(_levels[mx - 1][my] - reallevel)))) == 0.5) {
prim->add_vertex(min(max(sfav(y / level + 1, _levels[mx - 1][my], reallevel), 0), lowblocksize - 1));
prim->add_vertex(min(max(sfav(y / level - 1, _levels[mx - 1][my], reallevel), 0), lowblocksize - 1));
prim->add_vertex(vcounter);
prim->close_primitive();
}
} else if (_bruteforce ||
(!(y == level && x > level && x < _block_size && my > 0
&& _levels[mx][my - 1] > reallevel) &&
!(x == _block_size && mx < (_xsize - 1) / (_block_size) - 1
&& _levels[mx + 1][my] > reallevel) &&
!(x == _block_size && y > level && y < _block_size && mx < (_xsize - 1) / (_block_size) - 1
&& _levels[mx + 1][my] > reallevel) &&
!(y == _block_size && x > level && x < _block_size && my < (_ysize - 1) / (_block_size) - 1
&& _levels[mx][my + 1] > reallevel))) {
if ((x <= center && y <= center) || (x > center && y > center)) {
if (x > center) {
prim->add_vertex(vcounter - lowblocksize - 1);
prim->add_vertex(vcounter - 1);
prim->add_vertex(vcounter);
} else {
prim->add_vertex(vcounter);
prim->add_vertex(vcounter - lowblocksize);
prim->add_vertex(vcounter - lowblocksize - 1);
}
} else {
if (x > center) {
prim->add_vertex(vcounter);
prim->add_vertex(vcounter - lowblocksize);
prim->add_vertex(vcounter - 1);
} else {
prim->add_vertex(vcounter - 1);
prim->add_vertex(vcounter - lowblocksize);
prim->add_vertex(vcounter - lowblocksize - 1);
}
}
prim->close_primitive();
}
//right border
if (!_bruteforce && x == _block_size - level && mx < (_xsize - 1) / (_block_size) - 1 && _levels[mx + 1][my] > reallevel) {
if (y > level && y < _block_size - level + 1) {
prim->add_vertex(lowblocksize * (lowblocksize - 1) + min(max(sfav(y / level, _levels[mx + 1][my], reallevel), 0), lowblocksize - 1));
prim->add_vertex(vcounter);
prim->add_vertex(vcounter - 1);
prim->close_primitive();
}
if (f_part((y / level)/float(pow(2.0, int(_levels[mx + 1][my]-reallevel)))) == 0.5) {
prim->add_vertex(lowblocksize * (lowblocksize - 1) + min(max(sfav(y / level - 1, _levels[mx + 1][my], reallevel), 0), lowblocksize - 1));
prim->add_vertex(lowblocksize * (lowblocksize - 1) + min(max(sfav(y / level + 1, _levels[mx + 1][my], reallevel), 0), lowblocksize - 1));
prim->add_vertex(vcounter);
prim->close_primitive();
}
}
//bottom border
if (!_bruteforce && y == level && my > 0 && _levels[mx][my - 1] > reallevel) {
if (x > level && x < _block_size) {
prim->add_vertex(vcounter);
prim->add_vertex(vcounter - lowblocksize);
prim->add_vertex(min(max(sfav(x / level, _levels[mx][my - 1], reallevel), 0), lowblocksize - 1) * lowblocksize);
prim->close_primitive();
}
if (f_part((x / level)/float(pow(2.0, int(_levels[mx][my - 1]-reallevel)))) == 0.5) {
prim->add_vertex(min(max(sfav(x / level - 1, _levels[mx][my - 1], reallevel), 0), lowblocksize - 1) * lowblocksize);
prim->add_vertex(min(max(sfav(x / level + 1, _levels[mx][my - 1], reallevel), 0), lowblocksize - 1) * lowblocksize);
prim->add_vertex(vcounter);
prim->close_primitive();
}
} else if (_bruteforce || (!(x == level && y > level && y < _block_size && mx > 0 && _levels[mx - 1][my] > reallevel) && !(x == _block_size && y > level && y < _block_size && mx < (_xsize - 1) / (_block_size) - 1 && _levels[mx + 1][my] > reallevel) && !(x == _block_size && y > level && y < _block_size && mx < (_xsize - 1) / (_block_size) - 1 && _levels[mx + 1][my] > reallevel) && !(y == _block_size && my < (_ysize - 1) / (_block_size) - 1 && _levels[mx][my + 1] > reallevel))) {
if ((x <= center && y <= center) || (x > center && y > center)) {
if (y > center) {
prim->add_vertex(vcounter);
prim->add_vertex(vcounter - lowblocksize);//
prim->add_vertex(vcounter - lowblocksize - 1);
} else {
prim->add_vertex(vcounter - lowblocksize - 1);
prim->add_vertex(vcounter - 1);//
prim->add_vertex(vcounter);
}
} else {
if (y > center) {
prim->add_vertex(vcounter);//
prim->add_vertex(vcounter - lowblocksize);
prim->add_vertex(vcounter - 1);
} else {
prim->add_vertex(vcounter - 1);
prim->add_vertex(vcounter - lowblocksize);
prim->add_vertex(vcounter - lowblocksize - 1);//
}
}
prim->close_primitive();
}
//top border
if (!_bruteforce && y == _block_size - level && my < (_xsize - 1) / (_block_size) - 1 && _levels[mx][my + 1] > reallevel) {
if (x > level && x < _block_size - level + 1) {
prim->add_vertex(min(max(sfav(x / level, _levels[mx][my + 1], reallevel), 0), lowblocksize - 1) * lowblocksize + lowblocksize - 1);
prim->add_vertex(vcounter - lowblocksize);
prim->add_vertex(vcounter);
prim->close_primitive();
}
if (f_part((x / level)/float(pow(2.0, int(_levels[mx][my + 1]-reallevel)))) == 0.5) {
prim->add_vertex(min(max(sfav(x / level + 1, _levels[mx][my + 1], reallevel), 0), lowblocksize - 1) * lowblocksize + lowblocksize - 1);
prim->add_vertex(min(max(sfav(x / level - 1, _levels[mx][my + 1], reallevel), 0), lowblocksize - 1) * lowblocksize + lowblocksize - 1);
prim->add_vertex(vcounter);
prim->close_primitive();
}
}
}
vcounter++;
}
}
}
PT(Geom) geom = new Geom(vdata);
geom->add_primitive(prim);
PT(GeomNode) node = new GeomNode("gmm" + int_to_str(mx) + "x" + int_to_str(my));
node->add_geom(geom);
_old_levels.at(mx).at(my) = reallevel;
return NodePath(node);
}
////////////////////////////////////////////////////////////////////
// Function: GeoMipTerrain::get_elevation
// Access: Published
// Description: Fetches the elevation at (x, y), where the input
// coordinate is specified in pixels. This ignores
// the current LOD level and instead provides an
// accurate number. Linear blending is used for
// non-integral coordinates.
// Terrain scale is NOT taken into account! To get
// accurate normals, please multiply this with the
// terrain Z scale!
//
// trueElev = terr.get_elevation(x,y) * terr.get_sz();
////////////////////////////////////////////////////////////////////
double GeoMipTerrain::
get_elevation(double x, double y) {
y = (_ysize - 1) - y;
unsigned int xlo = (unsigned int) x;
unsigned int ylo = (unsigned int) y;
if (xlo < 0) xlo = 0;
if (ylo < 0) ylo = 0;
if (xlo > _xsize - 2)
xlo = _xsize - 2;
if (ylo > _ysize - 2)
ylo = _ysize - 2;
unsigned int xhi = xlo + 1;
unsigned int yhi = ylo + 1;
double xoffs = x - xlo;
double yoffs = y - ylo;
double grayxlyl = get_pixel_value(xlo, ylo);
double grayxhyl = get_pixel_value(xhi, ylo);
double grayxlyh = get_pixel_value(xlo, yhi);
double grayxhyh = get_pixel_value(xhi, yhi);
double lerpyl = grayxhyl * xoffs + grayxlyl * (1.0 - xoffs);
double lerpyh = grayxhyh * xoffs + grayxlyh * (1.0 - xoffs);
return lerpyh * yoffs + lerpyl * (1.0 - yoffs);
}
////////////////////////////////////////////////////////////////////
// Function: GeoMipTerrain::get_normal
// Access: Published
// Description: Fetches the terrain normal at (x, y), where the
// input coordinate is specified in pixels. This
// ignores the current LOD level and instead provides
// an accurate number.
// Terrain scale is NOT taken into account! To get
// accurate normals, please divide it by the
// terrain scale and normalize it again, like this:
//
// LVector3f normal (terr.get_normal(x, y));
// normal.set(normal.get_x() / root.get_sx(),
// normal.get_y() / root.get_sy(),
// normal.get_z() / root.get_sz());
// normal.normalize();
////////////////////////////////////////////////////////////////////
LVector3f GeoMipTerrain::
get_normal(int x, int y) {
int nx = x - 1;
int px = x + 1;
int ny = y - 1;
int py = y + 1;
if (nx < 0) nx++;
if (ny < 0) ny++;
if (px >= int(_xsize)) px--;
if (py >= int(_ysize)) py--;
double drx = get_pixel_value(px, y) - get_pixel_value(nx, y);
double dry = get_pixel_value(x, py) - get_pixel_value(x, ny);
LVector3f normal(drx * 0.5, dry * 0.5, 1);
normal.normalize();
return normal;
}
////////////////////////////////////////////////////////////////////
// Function: GeoMipTerrain::make_slope_image
// Access: Published
// Description: Returns a new grayscale image containing the slope
// angles. A pixel value of 1.0 will mean 90 degrees,
// meaning, horizontal, a pixel value of 0.0 will
// mean 0 degrees, or entirely vertical.
// The resulting image will have the same size as the
// heightfield image.
// The scale will be taken into respect -- meaning,
// if you change the terrain scale, the slope image
// will need to be regenerated in order to be correct.
////////////////////////////////////////////////////////////////////
PNMImage GeoMipTerrain::
make_slope_image() {
PNMImage result (_xsize, _ysize);
result.make_grayscale();
for (int x = 0; x < _xsize; ++x) {
for (int y = 0; y < _ysize; ++y) {
LVector3f normal (get_normal(x, y));
normal.set(normal.get_x() / _root.get_sx(),
normal.get_y() / _root.get_sy(),
normal.get_z() / _root.get_sz());
normal.normalize();
result.set_gray(x, y, normal.angle_deg(LVector3f::up()) / 90.0);
}
}
return result;
}
////////////////////////////////////////////////////////////////////
// Function: GeoMipTerrain::generate
// Access: Published
// Description: (Re)generates the entire terrain, erasing the
// current.
// This call un-flattens the terrain, so make sure
// you have set auto-flatten if you want to keep
// your terrain flattened.
////////////////////////////////////////////////////////////////////
void GeoMipTerrain::
generate() {
calc_levels();
_root.node()->remove_all_children();
_blocks.clear();
_old_levels.clear();
_old_levels.resize(int((_xsize - 1) / _block_size));
_root_flattened = false;
for (unsigned int mx = 0; mx < (_xsize - 1) / _block_size; mx++) {
_old_levels[mx].resize(int((_ysize - 1) / _block_size));
pvector<NodePath> tvector; //create temporary row
for (unsigned int my = 0; my < (_ysize - 1) / _block_size; my++) {
if (_bruteforce) {
tvector.push_back(generate_block(mx, my, 0));
} else {
tvector.push_back(generate_block(mx, my, _levels[mx][my]));
}
tvector[my].reparent_to(_root);
tvector[my].set_pos((mx + 0.5) * _block_size, (my + 0.5) * _block_size, 0);
}
_blocks.push_back(tvector); //push the new row of NodePaths into the 2d vect
tvector.clear();
}
auto_flatten();
_is_dirty = false;
}
////////////////////////////////////////////////////////////////////
// Function: GeoMipTerrain::update
// Access: Published
// Description: Loops through all of the terrain blocks, and
// checks whether they need to be updated.
// If that is indeed the case, it regenerates the
// mipmap. Returns a true when the terrain has
// changed. Returns false when the terrain isn't
// updated at all. If there is no terrain yet,
// it generates the entire terrain.
// This call un-flattens the terrain, so make sure
// you have set auto-flatten if you want to keep
// your terrain flattened.
////////////////////////////////////////////////////////////////////
bool GeoMipTerrain::
update() {
if (_is_dirty) {
generate();
return true;
} else if (!_bruteforce) {
calc_levels();
if (root_flattened()) {
_root.node()->remove_all_children();
unsigned int xsize = _blocks.size();
for (unsigned int tx = 0; tx < xsize; tx++) {
unsigned int ysize = _blocks[tx].size();
for (unsigned int ty = 0;ty < ysize; ty++) {
_blocks[tx][ty].reparent_to(_root);
}
}
_root_flattened = false;
}
bool returnVal = false;
for (unsigned int mx = 0; mx < (_xsize - 1) / _block_size; mx++) {
for (unsigned int my = 0; my < (_ysize - 1) / _block_size; my++) {
bool isUpd (update_block(mx, my));
if (isUpd && mx > 0 && _old_levels[mx - 1][my] == _levels[mx - 1][my]) {
if (update_block(mx - 1, my, -1, true)) {
returnVal = true;
}
}
if (isUpd && mx < (_ysize - 1)/_block_size - 1
&& _old_levels[mx + 1][my] == _levels[mx + 1][my]) {
if (update_block(mx + 1, my, -1, true)) {
returnVal = true;
}
}
if (isUpd && my > 0 && _old_levels[mx][my - 1] == _levels[mx][my - 1]) {
if (update_block(mx, my - 1, -1, true)) {
returnVal = true;
}
}
if (isUpd && my < (_ysize - 1)/_block_size - 1
&& _old_levels[mx][my + 1] == _levels[mx][my + 1]) {
if (update_block(mx, my + 1, -1, true)) {
returnVal = true;
}
}
if (isUpd) {
returnVal = true;
}
}
}
auto_flatten();
return returnVal;
}
return false;
}
////////////////////////////////////////////////////////////////////
// Function: GeoMipTerrain::root_flattened
// Access: Private
// Description: Normally, the root's children are the terrain blocks.
// However, if we call flatten_strong on the root,
// then the root will contain unpredictable stuff.
// This function returns true if the root has been
// flattened, and therefore, does not contain the
// terrain blocks.
////////////////////////////////////////////////////////////////////
bool GeoMipTerrain::
root_flattened() {
if (_root_flattened) {
return true;
}
// The following code is error-checking code. It actually verifies
// that the terrain blocks are underneath the root, and that nothing
// else is underneath the root. It is not very efficient, and should
// eventually be removed once we're sure everything works.
int total = 0;
unsigned int xsize = _blocks.size();
for (unsigned int tx = 0; tx < xsize; tx++) {
unsigned int ysize = _blocks[tx].size();
for (unsigned int ty = 0;ty < ysize; ty++) {
if (_blocks[tx][ty].get_node(1) != _root.node()) {
grutil_cat.error() << "GeoMipTerrain: root node unexpectedly mangled!\n";
return true;
}
total += 1;
}
}
if (total != _root.node()->get_num_children()) {
grutil_cat.error() << "GeoMipTerrain: root node unexpectedly mangled: " << total << " vs " << (_root.node()->get_num_children()) << "\n";
return true;
}
// The default.
return false;
}
////////////////////////////////////////////////////////////////////
// Function: GeoMipTerrain::auto_flatten
// Access: Private
// Description: Flattens the geometry under the root.
////////////////////////////////////////////////////////////////////
void GeoMipTerrain::
auto_flatten() {
if (_auto_flatten == AFM_off) {
return;
}
// Creating a backup node causes the SceneGraphReducer
// to operate in a nondestructive manner. This protects
// the terrain blocks themselves from the flattener.
NodePath np("Backup Node");
np.node()->copy_children(_root.node());
// Check if the root's children have changed unexpectedly.
switch(_auto_flatten) {
case AFM_light: _root.flatten_light(); break;
case AFM_medium: _root.flatten_medium(); break;
case AFM_strong: _root.flatten_strong(); break;
}
_root_flattened = true;
}
////////////////////////////////////////////////////////////////////
// Function: GeoMipTerrain::calc_levels
// Access: Private
// Description: Loops through all of the terrain blocks, and
// calculates on what level they should be generated.
////////////////////////////////////////////////////////////////////
void GeoMipTerrain::
calc_levels() {
_levels.clear();
for (unsigned int mx = 0; mx < (_xsize - 1) / _block_size; mx++) {
pvector<unsigned short> tvector; //create temporary row
for (unsigned int my = 0; my < (_ysize - 1) / _block_size; my++) {
if(_bruteforce) {
tvector.push_back(0);
} else {
tvector.push_back(min(short(max(_min_level, lod_decide(mx, my))),
short(_max_level)));
}
}
_levels.push_back(tvector); //push the new row of levels into the 2d vector
tvector.clear();
}
}
////////////////////////////////////////////////////////////////////
// Function: GeoMipTerrain::update_block
// Access: Private
// Description: Checks whether the specified mipmap at (mx,my)
// needs to be updated, if so, it regenerates the
// mipmap. Returns a true when it has generated
// a mipmap. Returns false when the mipmap is already
// at the desired level, or when there is no terrain
// to update. Note: This does not affect neighboring
// blocks, so does NOT fix t-junctions. You will have
// to fix that by forced updating the neighboring
// chunks as well, with the same levels.
// NOTE: do NOT call this when the terrain is marked
// dirty. If the terrain is dirty, you will need to
// call update() or generate() first.
// You can check this by calling GeoMipTerrain::is_dirty().
////////////////////////////////////////////////////////////////////
bool GeoMipTerrain::
update_block(unsigned short mx, unsigned short my,
signed short level, bool forced) {
nassertr_always(!_is_dirty, false);
nassertr_always(mx < (_xsize - 1) / _block_size, false);
nassertr_always(my < (_ysize - 1) / _block_size, false);
if (level == -1) {
level = _levels[mx][my];
}
if (forced || _old_levels[mx][my] != level) { // if the level has changed...
// this code copies the collision mask, removes the chunk and
// replaces it with a regenerated one.
CollideMask mask = _blocks[mx][my].get_collide_mask();
_blocks[mx][my].remove_node();
_blocks[mx][my] = generate_block(mx, my, level);
_blocks[mx][my].set_collide_mask(mask);
_blocks[mx][my].reparent_to(_root);
_blocks[mx][my].set_pos((mx + 0.5) * _block_size,
(my + 0.5) * _block_size, 0);
return true;
}
return false;
}