From 5000a700f10ffd1b97e9e727ce2aefdfd01df287 Mon Sep 17 00:00:00 2001 From: Josh Yelon Date: Thu, 10 Apr 2008 18:07:21 +0000 Subject: [PATCH] Added GeoMipTerrain --- panda/src/grutil/geoMipTerrain.I | 582 +++++++++++++++++++++++++ panda/src/grutil/geoMipTerrain.cxx | 561 ++++++++++++++++++++++++ panda/src/grutil/geoMipTerrain.h | 156 +++++++ panda/src/grutil/grutil_composite1.cxx | 1 + 4 files changed, 1300 insertions(+) create mode 100644 panda/src/grutil/geoMipTerrain.I create mode 100644 panda/src/grutil/geoMipTerrain.cxx create mode 100644 panda/src/grutil/geoMipTerrain.h diff --git a/panda/src/grutil/geoMipTerrain.I b/panda/src/grutil/geoMipTerrain.I new file mode 100644 index 0000000000..194f477e46 --- /dev/null +++ b/panda/src/grutil/geoMipTerrain.I @@ -0,0 +1,582 @@ +// Filename: geoMipTerrain.I +// Created by: pro-rsoft (29jun07) +// Last updated by: pro-rsoft (03mar08) +// +//////////////////////////////////////////////////////////////////// +// +// PANDA 3D SOFTWARE +// Copyright (c) 2001 - 2004, Disney Enterprises, Inc. All rights reserved +// +// All use of this software is subject to the terms of the Panda 3d +// Software license. You should have received a copy of this license +// along with this source code; you will also find a current copy of +// the license at http://etc.cmu.edu/panda3d/docs/license/ . +// +// To contact the maintainers of this program write to +// panda3d-general@lists.sourceforge.net . +// +//////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::Constructor +// Access: Published +// Description: +//////////////////////////////////////////////////////////////////// +INLINE GeoMipTerrain:: +GeoMipTerrain(const string &name) { + _root = NodePath(name); + _root_flattened = false; + _xsize = 0; + _ysize = 0; + _min_level = 0; + _block_size = 16; + _factor = 100.0; + _has_color_map = false; + PT(PandaNode) tmpnode = new PandaNode("tmp_focal"); + _auto_flatten = AFM_off; + _focal_point = NodePath(tmpnode); + _focal_is_temporary = true; + _is_dirty = true; + _bruteforce = false; +} + +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::Destructor +// Access: Published +// Description: This will not remove the terrain node itself. +// To have the terrain itself also deleted, please +// call remove_node() prior to destruction. +//////////////////////////////////////////////////////////////////// +INLINE GeoMipTerrain:: +~GeoMipTerrain() { +} + +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::heightfield +// Access: Published +// Description: Returns a reference to the heightfield (a PNMImage) +// contained inside GeoMipTerrain. You can use +// the reference to alter the heightfield. +//////////////////////////////////////////////////////////////////// +INLINE PNMImage &GeoMipTerrain:: +heightfield() { + return _heightfield; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::color_map +// Access: Published +// Description: Returns a reference to the color map (a PNMImage) +// contained inside GeoMipTerrain. You can use +// the reference to alter the color map. +//////////////////////////////////////////////////////////////////// +INLINE PNMImage &GeoMipTerrain:: +color_map() { + return _color_map; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::set_bruteforce +// Access: Published +// Description: Sets a boolean specifying whether the terrain will +// be rendered bruteforce. If the terrain is rendered +// bruteforce, there will be no Level of Detail, and +// the update() call will only update the +// terrain if it is marked dirty. +//////////////////////////////////////////////////////////////////// +INLINE void GeoMipTerrain:: +set_bruteforce(bool bf) { + if (bf == true && _bruteforce == false) { + _is_dirty = true; + } + _bruteforce = bf; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::get_bruteforce +// Access: Published +// Description: Returns a boolean whether the terrain is rendered +// bruteforce or not. See set_bruteforce for more +// information. +//////////////////////////////////////////////////////////////////// +INLINE bool GeoMipTerrain:: +get_bruteforce() { + return _bruteforce; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::set_auto_flatten +// Access: Private +// Description: The terrain can be automatically flattened (using +// flatten_light, flatten_medium, or flatten_strong) +// after each update. This only affects future +// updates, it doesn't flatten the current terrain. +// +//////////////////////////////////////////////////////////////////// +INLINE void GeoMipTerrain:: +set_auto_flatten(int mode) { + _auto_flatten = mode; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::set_focal_point +// Access: Published +// Description: Sets the focal point. GeoMipTerrain generates +// high-resolution terrain around the focal point, and +// progressively lower and lower resolution terrain +// as you get farther away. If a point is supplied +// and not a NodePath, make sure it's relative to +// the terrain. Only the x and y coordinates of +// the focal point are taken in respect. +//////////////////////////////////////////////////////////////////// +INLINE void GeoMipTerrain:: +set_focal_point(double x, double y) { + if (!_focal_is_temporary) { + PT(PandaNode) tmpnode = new PandaNode("tmp_focal"); + _focal_point = NodePath(tmpnode); + } + _focal_point.set_pos(_root, x, y, 0); + _focal_is_temporary = true; +} +INLINE void GeoMipTerrain:: +set_focal_point(LPoint2d fp) { + set_focal_point(fp.get_x(), fp.get_y()); +} +INLINE void GeoMipTerrain:: +set_focal_point(LPoint2f fp) { + set_focal_point(double(fp.get_x()), double(fp.get_y())); +} +INLINE void GeoMipTerrain:: +set_focal_point(LPoint3d fp) { + set_focal_point(fp.get_x(), fp.get_y()); +} +INLINE void GeoMipTerrain:: +set_focal_point(LPoint3f fp) { + set_focal_point(double(fp.get_x()), double(fp.get_y())); +} +INLINE void GeoMipTerrain:: +set_focal_point(NodePath fp) { + if (_focal_is_temporary) { + _focal_point.remove_node(); + } + _focal_point = fp; + _focal_is_temporary = false; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::get_focal_point +// Access: Published +// Description: Returns the focal point, as a NodePath. +// If you have set it to be just a point, it will +// return an empty node at the focal position. +//////////////////////////////////////////////////////////////////// +INLINE NodePath GeoMipTerrain:: +get_focal_point() const { + return _focal_point; +} + +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::get_root +// Access: Published +// Description: Returns the root of the terrain. This is a +// single PandaNode to which all the rest of the +// terrain is parented. The generate and update +// operations replace the nodes which are parented +// to this root, but they don't replace this root +// itself. +//////////////////////////////////////////////////////////////////// +INLINE NodePath GeoMipTerrain:: +get_root() const { + return _root; +} + +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::set_min_level +// Access: Published +// Description: Sets the minimum level of detail at which blocks +// may be generated by generate() or update(). +// The default value is 0, which is the highest +// quality. This value is also taken in respect when +// generating the terrain bruteforce. +//////////////////////////////////////////////////////////////////// +INLINE void GeoMipTerrain:: +set_min_level(unsigned short minlevel) { + _min_level = minlevel; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::get_min_level +// Access: Published +// Description: Gets the minimum level of detail at which blocks +// may be generated by generate() or update(). +// The default value is 0, which is the highest +// quality. +//////////////////////////////////////////////////////////////////// +INLINE unsigned short GeoMipTerrain:: +get_min_level() { + return _min_level; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::get_block_size +// Access: Published +// Description: Gets the block size. +//////////////////////////////////////////////////////////////////// +INLINE unsigned short GeoMipTerrain:: +get_block_size() { + return _block_size; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::set_block_size +// Access: Published +// Description: Sets the block size. If it is not a power of two, +// the closest power of two is used. +//////////////////////////////////////////////////////////////////// +INLINE void GeoMipTerrain:: +set_block_size(unsigned short newbs) { + if (is_power_of_two(newbs)) { + _block_size = newbs; + } else { + if (is_power_of_two(newbs - 1)) { + _block_size = newbs - 1; + } else { + if (is_power_of_two(newbs + 1)) { + _block_size = newbs + 1; + } else { + _block_size = (unsigned short) pow(2.0, + floor(log(float(newbs)) / log(2.0) + 0.5)); + } + } + } + _is_dirty = true; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::is_dirty +// Access: Published +// Description: Returns a bool indicating whether the terrain is +// marked 'dirty', that means the terrain has to be +// regenerated on the next update() call, because +// for instance the heightfield has changed. +// Once the terrain has been regenerated, the dirty +// flag automatically gets reset internally. +//////////////////////////////////////////////////////////////////// +INLINE bool GeoMipTerrain:: +is_dirty() { + return _is_dirty; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::set_factor +// Access: Published +// Description: Sets the quality factor at which blocks must be +// generated. The higher this level, the better +// quality the terrain will be, but more expensive +// to render. A value of 0 makes the terrain the +// lowest quality possible, depending on blocksize. +// The default value is 100. +//////////////////////////////////////////////////////////////////// +INLINE void GeoMipTerrain:: +set_factor(float factor) { + _factor = factor; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::get_factor +// Access: Published +// Description: Gets the quality factor at which blocks must be +// generated. The higher this level, the better +// quality the terrain will be, but more expensive +// to render. A value of 0 makes the terrain the +// lowest quality possible, depending on blocksize. +// The default value is 100. +//////////////////////////////////////////////////////////////////// +INLINE float GeoMipTerrain:: +get_factor() { + return _factor; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::get_block_node_path +// Access: Published +// Description: Returns the NodePath of the specified block. +// If auto-flatten is enabled and the node is +// getting removed during the flattening process, +// it will still return a NodePath with the +// appropriate terrain chunk, but it will be in +// a temporary scenegraph. +// Please note that this returns a const object and +// you can not modify the node. Modify the heightfield +// instead. +//////////////////////////////////////////////////////////////////// +INLINE const NodePath GeoMipTerrain:: +get_block_node_path(unsigned short mx, unsigned short my) { + return _blocks[mx][my]; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::get_block_from_pos +// Access: Published +// Description: Gets the coordinates of the block at the specified +// position. This position must be relative to the +// terrain, not to render. Returns an array containing +// two values: the block x and the block y coords. +// If the positions are out of range, the closest +// block is taken. +// Note that the VecBase returned does not represent +// a vector, position, or rotation, but it contains +// the block index of the block which you can use +// in GeoMipTerrain::get_block_node_path. +//////////////////////////////////////////////////////////////////// +INLINE LVecBase2f GeoMipTerrain:: +get_block_from_pos(double x, double y) { + if (x < 0) x = 0; + if (y < 0) y = 0; + if (x > _xsize - 1) x = _xsize - 1; + if (y > _ysize - 1) y = _ysize - 1; + x = floor(x / _block_size); + y = floor(y / _block_size); + return LVecBase2f(x, y); +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::lod_decide +// Access: Private +// Description: Calculates the level for the given mipmap. +//////////////////////////////////////////////////////////////////// +INLINE unsigned short GeoMipTerrain:: +lod_decide(unsigned short mx, unsigned short my) { + float cx = mx; + float cy = my; + cx = (cx * _block_size + _block_size / 2) * _root.get_sx(); + cy = (cy * _block_size + _block_size / 2) * _root.get_sy(); + float d; + if (_factor > 0.0) { + d = sqrt(pow(_focal_point.get_x(_root) - cx, 2) + + pow(_focal_point.get_y(_root) - cy, 2)) / _factor; + } else { + d = log(float(_block_size)) / log(2.0); + } + return short(floor(d)); +} + +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::set_heightfield +// Access: Published +// Description: Loads the specified heightmap image file into +// the heightfield. Returns true if succeeded, or +// false if an error has occured. +// If the heightmap is not a power of two plus one, +// it is scaled up using a gaussian filter. +//////////////////////////////////////////////////////////////////// +INLINE bool GeoMipTerrain:: +set_heightfield(const Filename &filename, PNMFileType *ftype) { + PNMImage image; + if (image.read(filename, ftype)) { + _is_dirty = true; + _heightfield = PNMImage( + max(3, (int) pow(2.0, ceil(log(float(max(2, image.get_x_size()))) + / log(2.0))) + 1), + max(3, (int) pow(2.0, ceil(log(float(max(2, image.get_y_size()))) + / log(2.0))) + 1)); + // Make sure not to apply gaussian when it's already the right size + if (_heightfield.get_x_size() == image.get_x_size() && + _heightfield.get_y_size() == image.get_y_size()) { + _heightfield.copy_from(image); + } else { + _heightfield.gaussian_filter_from(1.0, image); + } + _xsize = _heightfield.get_x_size(); + _ysize = _heightfield.get_y_size(); + return true; + } + return false; +} +INLINE bool GeoMipTerrain:: +set_heightfield(const PNMImage &image) { + _heightfield = PNMImage( + max(3, (int) pow(2.0, ceil(log(float(max(2, image.get_x_size()))) + / log(2.0))) + 1), + max(3, (int) pow(2.0, ceil(log(float(max(2, image.get_y_size()))) + / log(2.0))) + 1)); + // Make sure not to apply gaussian when it's already the right size + if (_heightfield.get_x_size() == image.get_x_size() && + _heightfield.get_y_size() == image.get_y_size()) { + _heightfield.copy_from(image); + } else { + _heightfield.gaussian_filter_from(1.0, image); + } + _is_dirty = true; + _xsize = _heightfield.get_x_size(); + _ysize = _heightfield.get_y_size(); + return true; +} +INLINE bool GeoMipTerrain:: +set_heightfield(const Texture *tex) { + _heightfield = PNMImage( + max(3, (int) pow(2.0, ceil(log(float(max(2, tex->get_x_size()))) + / log(2.0))) + 1), + max(3, (int) pow(2.0, ceil(log(float(max(2, tex->get_y_size()))) + / log(2.0))) + 1)); + PNMImage image; + tex->store(image); + // Make sure not to apply gaussian when it's already the right size + if (_heightfield.get_x_size() == image.get_x_size() && + _heightfield.get_y_size() == image.get_y_size()) { + _heightfield.copy_from(image); + } else { + _heightfield.gaussian_filter_from(1.0, image); + } + _is_dirty = true; + _xsize = _heightfield.get_x_size(); + _ysize = _heightfield.get_y_size(); + return true; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::set_color_map +// Access: Published +// Description: Loads the specified image as color map. The next +// time generate() is called, the terrain is painted +// with this color map using the vertex color column. +// Returns a boolean indicating whether the operation +// has succeeded. +//////////////////////////////////////////////////////////////////// +INLINE bool GeoMipTerrain:: +set_color_map(const Filename &filename, PNMFileType *ftype) { + if (_color_map.read(filename, ftype)) { + _is_dirty = true; + _has_color_map = true; + return true; + } + return false; +} +INLINE bool GeoMipTerrain:: +set_color_map(const PNMImage &image) { + _color_map.copy_from(image); + _is_dirty = true; + _has_color_map = true; + return true; +} +INLINE bool GeoMipTerrain:: +set_color_map(const Texture *tex) { + tex->store(_color_map); + _is_dirty = true; + return true; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::has_color_map +// Access: Published +// Description: Returns whether a color map has been set. +//////////////////////////////////////////////////////////////////// +INLINE bool GeoMipTerrain:: +has_color_map() { + return _has_color_map; +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::clear_color_map +// Access: Published +// Description: Clears the color map. +//////////////////////////////////////////////////////////////////// +INLINE void GeoMipTerrain:: +clear_color_map() { + if (_has_color_map) { + _color_map.clear(); + _has_color_map = false; + } +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::get_pixel_value +// Access: Private +// Description: Get the elevation at a certain pixel of the image. +// This function does NOT linearly interpolate. +// For that, use GeoMipTerrain::get_elevation() instead. +//////////////////////////////////////////////////////////////////// +INLINE double GeoMipTerrain:: +get_pixel_value(int x, int y) { + x = max(min(x,int(_xsize-1)),0); + y = max(min(y,int(_ysize-1)),0); + return double(_heightfield.get_bright(int(x),int(y))); +/* return double(_heightfield.get_red_val(int(x),int(y)) + + _heightfield.get_green_val(int(x),int(y)) * 256 + + _heightfield.get_blue_val(int(x),int(y)) * 65536) / 16777215.0; +*/ +} +INLINE double GeoMipTerrain:: +get_pixel_value(unsigned short mx, unsigned short my, int x, int y) { + nassertr_always(mx < (_xsize - 1) / _block_size, false); + nassertr_always(my < (_ysize - 1) / _block_size, false); + return get_pixel_value(mx * _block_size + x, (_ysize - 1) - + (my * _block_size + y)); +} + +//////////////////////////////////////////////////////////////////// +// 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! +//////////////////////////////////////////////////////////////////// +INLINE LVector3f GeoMipTerrain:: +get_normal(unsigned short mx, unsigned short my, int x, int y) { + nassertr_always(mx < (_xsize - 1) / _block_size, false); + nassertr_always(my < (_ysize - 1) / _block_size, false); + return get_normal(mx * _block_size + x, (_ysize - 1) - + (my * _block_size + y)); +} + +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::int2str +// Access: Private +// Description: Converts the given int to a std::string. +//////////////////////////////////////////////////////////////////// +INLINE std::string GeoMipTerrain:: +int_to_str(int i) { + std::stringstream ss; + std::string str; + ss << i; + ss >> str; + return str; +} + +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::str2int +// Access: Private +// Description: Converts the given std::string to an int. +//////////////////////////////////////////////////////////////////// +INLINE int GeoMipTerrain:: +str_to_int(std::string str) { + std::istringstream strin(str); + int i; + strin >> i; + return i; +} + +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::is_power_of_two +// Access: Private +// Description: Returns a bool whether the given int i is a +// power of two or not. +//////////////////////////////////////////////////////////////////// +INLINE bool GeoMipTerrain:: +is_power_of_two(unsigned int i) { + return !((i - 1) & i); +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::f_part +// Access: Private +// Description: Returns the part of the number right of the +// floating-point. +//////////////////////////////////////////////////////////////////// +INLINE float GeoMipTerrain:: +f_part(float i) { + return i - floor(i); +} +INLINE double GeoMipTerrain:: +f_part(double i) { + return i - floor(i); +} +//////////////////////////////////////////////////////////////////// +// Function: GeoMipTerrain::sfav +// Access: Private +// Description: Used to calculate vertex numbers. Only to +// be used internally. +//////////////////////////////////////////////////////////////////// +INLINE int GeoMipTerrain:: +sfav(int n, int powlevel, int mypowlevel) { + double t = n - 1; + t /= float(pow(2.0, powlevel - mypowlevel)); + t = double(int(t > 0.0 ? t + 0.5 : t - 0.5)); + t *= float(pow(2.0, powlevel - mypowlevel)); + return int(t); +} + diff --git a/panda/src/grutil/geoMipTerrain.cxx b/panda/src/grutil/geoMipTerrain.cxx new file mode 100644 index 0000000000..93de760c53 --- /dev/null +++ b/panda/src/grutil/geoMipTerrain.cxx @@ -0,0 +1,561 @@ +// Filename: geoMipTerrain.cxx +// Created by: pro-rsoft (29jun07) +// Last updated by: pro-rsoft (08mar08) +// +//////////////////////////////////////////////////////////////////// +// +// PANDA 3D SOFTWARE +// Copyright (c) 2001 - 2004, Disney Enterprises, Inc. All rights reserved +// +// All use of this software is subject to the terms of the Panda 3d +// Software license. You should have received a copy of this license +// along with this source code; you will also find a current copy of +// the license at http://etc.cmu.edu/panda3d/docs/license/ . +// +// To contact the maintainers of this program write to +// panda3d-general@lists.sourceforge.net . +// +//////////////////////////////////////////////////////////////////// + +#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 "sceneGraphReducer.h" + +#include "collideMask.h" + +//////////////////////////////////////////////////////////////////// +// 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_dynamic); + 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_dynamic); + + 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(log(float(_block_size)) + / log(2.0))); + 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) { + LVector3f normal (get_normal(mx, my, x, y)); + normal.set(normal.get_x() / _root.get_sx(), + normal.get_y() / _root.get_sy(), + normal.get_z() / _root.get_sz()); + normal.normalize(); + 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(normal); + 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(mx, my, x, y)); +// normal.set(normal.get_x() / terr.get_sx(), +// normal.get_y() / terr.get_sy(), +// normal.get_z() / terr.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::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() { + if (!_bruteforce) { + 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 tvector; //create temporary row + for (unsigned int my = 0; my < (_ysize - 1) / _block_size; my++) { + 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!\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(); + unsigned short t; + for (unsigned int mx = 0; mx < (_xsize - 1) / _block_size; mx++) { + pvector tvector; //create temporary row + pvector tvector2; //create temporary row + for (unsigned int my = 0; my < (_ysize - 1) / _block_size; my++) { + t = min(short(max(_min_level, lod_decide(mx, my))), + short(log(float(_block_size)) / log(2.0))); + tvector.push_back(t); + } + _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; +} + diff --git a/panda/src/grutil/geoMipTerrain.h b/panda/src/grutil/geoMipTerrain.h new file mode 100644 index 0000000000..1f5a17c80d --- /dev/null +++ b/panda/src/grutil/geoMipTerrain.h @@ -0,0 +1,156 @@ +// Filename: geoMipTerrain.h +// Created by: pro-rsoft (29jun07) +// Last updated by: pro-rsoft (03mar08) +// +//////////////////////////////////////////////////////////////////// +// +// PANDA 3D SOFTWARE +// Copyright (c) 2001 - 2004, Disney Enterprises, Inc. All rights reserved +// +// All use of this software is subject to the terms of the Panda 3d +// Software license. You should have received a copy of this license +// along with this source code; you will also find a current copy of +// the license at http://etc.cmu.edu/panda3d/docs/license/ . +// +// To contact the maintainers of this program write to +// panda3d-general@lists.sourceforge.net . +// +//////////////////////////////////////////////////////////////////// + +#ifndef GEOMIPTERRAIN_H +#define GEOMIPTERRAIN_H + +#include "pandabase.h" + +#include "luse.h" +#include "pandaNode.h" +#include "pointerTo.h" + +#include "pnmImage.h" +#include "nodePath.h" + +#include "texture.h" + +//////////////////////////////////////////////////////////////////// +// Class : GeoMipTerrain +// Description : GeoMipTerrain, meaning Panda3D GeoMipMapping, can convert +// a heightfield image into a 3D terrain, consisting +// of several GeomNodes. It uses the GeoMipMapping +// algorithm, or Geometrical MipMapping, based on +// the LOD (Level of Detail) algorithm. For more +// information about the GeoMipMapping algoritm, see +// this paper, written by Willem H. de Boer: +// http://flipcode.com/articles/article_geomipmaps.pdf +// +//////////////////////////////////////////////////////////////////// +class GeoMipTerrain { +PUBLISHED: + INLINE GeoMipTerrain(const string &name); + INLINE ~GeoMipTerrain(); + + INLINE PNMImage &heightfield(); + INLINE bool set_heightfield(const Filename &filename, + PNMFileType *type = NULL); + INLINE bool set_heightfield(const PNMImage &image); + INLINE bool set_heightfield(const Texture *image); + INLINE PNMImage &color_map(); + INLINE bool set_color_map(const Filename &filename, + PNMFileType *type = NULL); + INLINE bool set_color_map(const PNMImage &image); + INLINE bool set_color_map(const Texture *image); + INLINE bool has_color_map(); + INLINE void clear_color_map(); + double get_elevation(double x, double y); + LVector3f get_normal(int x, int y); + INLINE LVector3f get_normal(unsigned short mx, unsigned short my, + int x,int y); + INLINE void set_bruteforce(bool bf); + INLINE bool get_bruteforce(); + + // The flatten mode specifies whether the terrain nodes are flattened + // together after each terrain update. + enum AutoFlattenMode { + // FM_off: don't ever flatten the terrain. + AFM_off = 0, + // FM_light: the terrain is flattened using flatten_light. + AFM_light = 1, + // FM_medium: the terrain is flattened using flatten_medium. + AFM_medium = 2, + // FM_strong: the terrain is flattened using flatten_strong. + AFM_strong = 3, + }; + + INLINE void set_auto_flatten(int mode); + + // The focal point is the point at which the terrain will have the + // lowest level of detail (highest quality). Parts farther away + // from the focal point will hae a higher level of detail. The + // focal point is not taken in respect if bruteforce is set true. + INLINE void set_focal_point(LPoint2d fp); + INLINE void set_focal_point(LPoint2f fp); + INLINE void set_focal_point(LPoint3d fp); + INLINE void set_focal_point(LPoint3f fp); + INLINE void set_focal_point(double x, double y); + INLINE void set_focal_point(NodePath fnp); + INLINE NodePath get_focal_point() const; + INLINE NodePath get_root() const; + + INLINE void set_min_level(unsigned short minlevel); + INLINE unsigned short get_min_level(); + INLINE unsigned short get_block_size(); + INLINE void set_block_size(unsigned short newbs); + INLINE bool is_dirty(); + INLINE float get_factor(); + INLINE void set_factor(float factor); + INLINE const NodePath get_block_node_path(unsigned short mx, + unsigned short my); + INLINE LVecBase2f get_block_from_pos(double x, double y); + + void generate(); + bool update(); + +private: + + + NodePath generate_block(unsigned short mx, unsigned short my, unsigned short level); + bool update_block(unsigned short mx, unsigned short my, + signed short level = -1, bool forced = false); + void calc_levels(); + void auto_flatten(); + bool root_flattened(); + + INLINE std::string int_to_str(int i); + INLINE int str_to_int(std::string str); + INLINE bool is_power_of_two(unsigned int i); + INLINE float f_part(float i); + INLINE double f_part(double i); + INLINE int sfav(int n, int powlevel, int mypowlevel); + INLINE double get_pixel_value(int x, int y); + INLINE double get_pixel_value(unsigned short mx, unsigned short my, int x, int y); + INLINE unsigned short lod_decide(unsigned short mx, unsigned short my); + + NodePath _root; + int _auto_flatten; + bool _root_flattened; + PNMImage _heightfield; + PNMImage _color_map; + bool _is_dirty; + bool _has_color_map; + unsigned int _xsize; + unsigned int _ysize; + float _factor; + unsigned short _block_size; + bool _bruteforce; + NodePath _focal_point; + bool _focal_is_temporary; + unsigned short _min_level; + pvector > _blocks; + pvector > _levels; + pvector > _old_levels; + +}; + +#include "geoMipTerrain.I" + +#endif /*GEOMIPTERRAIN_H*/ + diff --git a/panda/src/grutil/grutil_composite1.cxx b/panda/src/grutil/grutil_composite1.cxx index 38d140d536..bda082d74b 100644 --- a/panda/src/grutil/grutil_composite1.cxx +++ b/panda/src/grutil/grutil_composite1.cxx @@ -1,6 +1,7 @@ #include "cardMaker.cxx" #include "arToolKit.cxx" #include "heightfieldTesselator.cxx" +#include "geoMipTerrain.cxx" #include "config_grutil.cxx" #include "lineSegs.cxx" #include "fisheyeMaker.cxx"