518 lines
18 KiB
C++
518 lines
18 KiB
C++
// Filename: stBasicTerrain.cxx
|
|
// Created by: drose (12Oct10)
|
|
//
|
|
////////////////////////////////////////////////////////////////////
|
|
//
|
|
// 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 "stBasicTerrain.h"
|
|
#include "geomVertexWriter.h"
|
|
#include "pnmImage.h"
|
|
#include "indent.h"
|
|
|
|
TypeHandle STBasicTerrain::_type_handle;
|
|
|
|
// VERTEX_ATTRIB_END is defined as a macro that must be evaluated
|
|
// within the SpeedTree namespace.
|
|
namespace SpeedTree {
|
|
static const SVertexAttribDesc st_attrib_end = VERTEX_ATTRIB_END();
|
|
}
|
|
|
|
/* Hmm, maybe we want to use this lower-level structure directly
|
|
instead of the GeomVertexWriter.
|
|
|
|
namespace SpeedTree {
|
|
static const SVertexAttribDesc std_vertex_format[] = {
|
|
{ VERTEX_ATTRIB_SEMANTIC_POS, VERTEX_ATTRIB_TYPE_FLOAT, 3 },
|
|
{ VERTEX_ATTRIB_SEMANTIC_TEXCOORD0, VERTEX_ATTRIB_TYPE_FLOAT, 3 },
|
|
VERTEX_ATTRIB_END( )
|
|
};
|
|
static const int std_vertex_format_length =
|
|
sizeof(std_vertex_format) / sizeof(std_vertex_format[0]);
|
|
};
|
|
*/
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::Constructor
|
|
// Access: Published
|
|
// Description:
|
|
////////////////////////////////////////////////////////////////////
|
|
STBasicTerrain::
|
|
STBasicTerrain() {
|
|
clear();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::Copy Constructor
|
|
// Access: Published
|
|
// Description: Not sure whether any derived classes will implement
|
|
// the copy constructor, but it's defined here at the
|
|
// base level just in case.
|
|
////////////////////////////////////////////////////////////////////
|
|
STBasicTerrain::
|
|
STBasicTerrain(const STBasicTerrain ©) :
|
|
STTerrain(copy),
|
|
_size(copy._size),
|
|
_height_scale(copy._height_scale)
|
|
{
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::Destructor
|
|
// Access: Published, Virtual
|
|
// Description:
|
|
////////////////////////////////////////////////////////////////////
|
|
STBasicTerrain::
|
|
~STBasicTerrain() {
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::clear
|
|
// Access: Published, Virtual
|
|
// Description: Resets the terrain to its initial, unloaded state.
|
|
////////////////////////////////////////////////////////////////////
|
|
void STBasicTerrain::
|
|
clear() {
|
|
STTerrain::clear();
|
|
|
|
_height_map = "";
|
|
_size = 1.0f;
|
|
_height_scale = 1.0f;
|
|
|
|
CPT(GeomVertexFormat) format = GeomVertexFormat::register_format
|
|
(new GeomVertexArrayFormat(InternalName::get_vertex(), 3,
|
|
GeomEnums::NT_stdfloat, GeomEnums::C_point,
|
|
InternalName::get_texcoord(), 3,
|
|
GeomEnums::NT_stdfloat, GeomEnums::C_texcoord));
|
|
set_vertex_format(format);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::setup_terrain
|
|
// Access: Published
|
|
// Description: Sets up the terrain by reading a terrain.txt file as
|
|
// defined by SpeedTree. This file names the various
|
|
// map files that define the terrain, as well as
|
|
// defining parameters size as its size and color.
|
|
//
|
|
// If a relative filename is supplied, the model-path is
|
|
// searched. If a directory is named, "terrain.txt" is
|
|
// implicitly appended.
|
|
////////////////////////////////////////////////////////////////////
|
|
bool STBasicTerrain::
|
|
setup_terrain(const Filename &terrain_filename) {
|
|
_is_valid = false;
|
|
set_name(terrain_filename.get_basename());
|
|
|
|
VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr();
|
|
|
|
Filename fullpath = Filename::text_filename(terrain_filename);
|
|
vfs->resolve_filename(fullpath, get_model_path());
|
|
|
|
if (!vfs->exists(fullpath)) {
|
|
speedtree_cat.warning()
|
|
<< "Couldn't find " << terrain_filename << "\n";
|
|
return false;
|
|
}
|
|
|
|
if (vfs->is_directory(fullpath)) {
|
|
fullpath = Filename(fullpath, "terrain.txt");
|
|
}
|
|
|
|
istream *in = vfs->open_read_file(fullpath, true);
|
|
if (in == NULL) {
|
|
speedtree_cat.warning()
|
|
<< "Couldn't open " << terrain_filename << "\n";
|
|
return false;
|
|
}
|
|
|
|
bool success = setup_terrain(*in, fullpath);
|
|
vfs->close_read_file(in);
|
|
|
|
return success;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::setup_terrain
|
|
// Access: Published
|
|
// Description: Sets up the terrain by reading a terrain.txt file as
|
|
// defined by SpeedTree. This variant on this method
|
|
// accepts an istream for an already-opened terrain.txt
|
|
// file. The filename is provided for reference, to
|
|
// assist relative file operations. It should name the
|
|
// terrain.txt file that has been opened.
|
|
////////////////////////////////////////////////////////////////////
|
|
bool STBasicTerrain::
|
|
setup_terrain(istream &in, const Filename &pathname) {
|
|
clear();
|
|
|
|
Filename dirname = pathname.get_dirname();
|
|
|
|
string keyword;
|
|
in >> keyword;
|
|
while (in && !in.eof()) {
|
|
if (keyword == "area") {
|
|
// "area" defines the size of the terrain in square kilometers.
|
|
// We apply speedtree_area_scale to convert that to local units.
|
|
PN_stdfloat area;
|
|
in >> area;
|
|
_size = csqrt(area) * speedtree_area_scale;
|
|
|
|
} else if (keyword == "height_scale") {
|
|
in >> _height_scale;
|
|
|
|
} else if (keyword == "normalmap_b_scale") {
|
|
PN_stdfloat normalmap_b_scale;
|
|
in >> normalmap_b_scale;
|
|
|
|
} else if (keyword == "heightmap") {
|
|
read_quoted_filename(_height_map, in, dirname);
|
|
|
|
} else if (keyword == "texture") {
|
|
SplatLayer splat;
|
|
read_quoted_filename(splat._filename, in, dirname);
|
|
in >> splat._tiling;
|
|
splat._color.set(1.0f, 1.0f, 1.0f, 1.0f);
|
|
_splat_layers.push_back(splat);
|
|
|
|
} else if (keyword == "color") {
|
|
// color means the overall color of the previous texture.
|
|
PN_stdfloat r, g, b;
|
|
in >> r >> g >> b;
|
|
if (!_splat_layers.empty()) {
|
|
_splat_layers.back()._color.set(r, g, b, 1.0f);
|
|
}
|
|
|
|
} else if (keyword == "ambient" || keyword == "diffuse" || keyword == "specular" || keyword == "emissive") {
|
|
PN_stdfloat r, g, b;
|
|
in >> r >> g >> b;
|
|
|
|
} else if (keyword == "shininess") {
|
|
PN_stdfloat s;
|
|
in >> s;
|
|
|
|
} else {
|
|
speedtree_cat.error()
|
|
<< "Invalid token " << keyword << " in " << pathname << "\n";
|
|
return false;
|
|
}
|
|
|
|
in >> keyword;
|
|
}
|
|
|
|
// Consume any whitespace at the end of the file.
|
|
in >> ws;
|
|
|
|
if (!in.eof()) {
|
|
// If we didn't read all the way to end-of-file, there was an
|
|
// error.
|
|
in.clear();
|
|
string text;
|
|
in >> text;
|
|
speedtree_cat.error()
|
|
<< "Unexpected text in " << pathname << " at \"" << text << "\"\n";
|
|
return false;
|
|
}
|
|
|
|
// The first two textures are the normal map and splat map,
|
|
// respectively.
|
|
if (!_splat_layers.empty()) {
|
|
_normal_map = _splat_layers[0]._filename;
|
|
_splat_layers.erase(_splat_layers.begin());
|
|
}
|
|
if (!_splat_layers.empty()) {
|
|
_splat_map = _splat_layers[0]._filename;
|
|
_splat_layers.erase(_splat_layers.begin());
|
|
}
|
|
|
|
// Now try to load the actual height map data.
|
|
load_data();
|
|
|
|
return _is_valid;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::load_data
|
|
// Access: Published, Virtual
|
|
// Description: This will be called at some point after
|
|
// initialization. It should be overridden by a derived
|
|
// class to load up the terrain data from its source and
|
|
// fill in the data members of this class appropriately,
|
|
// especially _is_valid. After this call, if _is_valid
|
|
// is true, then get_height() etc. will be called to
|
|
// query the terrain's data.
|
|
////////////////////////////////////////////////////////////////////
|
|
void STBasicTerrain::
|
|
load_data() {
|
|
_is_valid = false;
|
|
|
|
if (!read_height_map()) {
|
|
return;
|
|
}
|
|
|
|
_is_valid = true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::get_height
|
|
// Access: Published, Virtual
|
|
// Description: After load_data() has been called, this should return
|
|
// the computed height value at point (x, y) of the
|
|
// terrain, where x and y are unbounded and may refer to
|
|
// any 2-d point in space.
|
|
////////////////////////////////////////////////////////////////////
|
|
PN_stdfloat STBasicTerrain::
|
|
get_height(PN_stdfloat x, PN_stdfloat y) const {
|
|
return _height_data.calc_bilinear_interpolation(x / _size, y / _size);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::get_smooth_height
|
|
// Access: Published, Virtual
|
|
// Description: After load_data() has been called, this should return
|
|
// the approximate average height value over a circle of
|
|
// the specified radius, centered at point (x, y) of the
|
|
// terrain.
|
|
////////////////////////////////////////////////////////////////////
|
|
PN_stdfloat STBasicTerrain::
|
|
get_smooth_height(PN_stdfloat x, PN_stdfloat y, PN_stdfloat radius) const {
|
|
return _height_data.calc_smooth(x / _size, y / _size, radius / _size);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::get_slope
|
|
// Access: Published, Virtual
|
|
// Description: After load_data() has been called, this should return
|
|
// the directionless slope at point (x, y) of the
|
|
// terrain, where 0.0 is flat and 1.0 is vertical. This
|
|
// is used for determining the legal points to place
|
|
// trees and grass.
|
|
////////////////////////////////////////////////////////////////////
|
|
PN_stdfloat STBasicTerrain::
|
|
get_slope(PN_stdfloat x, PN_stdfloat y) const {
|
|
return _slope_data.calc_bilinear_interpolation(x / _size, y / _size);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::fill_vertices
|
|
// Access: Published, Virtual
|
|
// Description: After load_data() has been called, this will be
|
|
// called occasionally to populate the vertices for a
|
|
// terrain cell.
|
|
//
|
|
// It will be passed a GeomVertexData whose format will
|
|
// match get_vertex_format(), and already allocated with
|
|
// num_xy * num_xy rows. This method should fill the
|
|
// rows of the data with the appropriate vertex data for
|
|
// the terrain, over the grid described by the corners
|
|
// (start_x, start_y) up to and including (start_x +
|
|
// size_x, start_y + size_xy)--a square of the terrain
|
|
// with num_xy vertices on a size, arranged in row-major
|
|
// order.
|
|
////////////////////////////////////////////////////////////////////
|
|
void STBasicTerrain::
|
|
fill_vertices(GeomVertexData *data,
|
|
PN_stdfloat start_x, PN_stdfloat start_y,
|
|
PN_stdfloat size_xy, int num_xy) const {
|
|
nassertv(data->get_format() == _vertex_format);
|
|
GeomVertexWriter vertex(data, InternalName::get_vertex());
|
|
GeomVertexWriter texcoord(data, InternalName::get_texcoord());
|
|
|
|
PN_stdfloat vertex_scale = 1.0 / (PN_stdfloat)(num_xy - 1);
|
|
PN_stdfloat texcoord_scale = 1.0 / _size;
|
|
for (int xi = 0; xi < num_xy; ++xi) {
|
|
PN_stdfloat xt = xi * vertex_scale;
|
|
PN_stdfloat x = start_x + xt * size_xy;
|
|
for (int yi = 0; yi < num_xy; ++yi) {
|
|
PN_stdfloat yt = yi * vertex_scale;
|
|
PN_stdfloat y = start_y + yt * size_xy;
|
|
|
|
PN_stdfloat z = get_height(x, y);
|
|
|
|
vertex.set_data3(x, y, z);
|
|
texcoord.set_data3(x * texcoord_scale, -y * texcoord_scale, 1.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::output
|
|
// Access: Published, Virtual
|
|
// Description:
|
|
////////////////////////////////////////////////////////////////////
|
|
void STBasicTerrain::
|
|
output(ostream &out) const {
|
|
Namable::output(out);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::write
|
|
// Access: Published, Virtual
|
|
// Description:
|
|
////////////////////////////////////////////////////////////////////
|
|
void STBasicTerrain::
|
|
write(ostream &out, int indent_level) const {
|
|
indent(out, indent_level)
|
|
<< *this << "\n";
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::read_height_map
|
|
// Access: Protected
|
|
// Description: Reads the height map image stored in _height_map, and
|
|
// stores it in _height_data. Returns true on success,
|
|
// false on failure.
|
|
////////////////////////////////////////////////////////////////////
|
|
bool STBasicTerrain::
|
|
read_height_map() {
|
|
PNMImage image(_height_map);
|
|
if (!image.is_valid()) {
|
|
return false;
|
|
}
|
|
|
|
_height_data.reset(image.get_x_size(), image.get_y_size());
|
|
_min_height = FLT_MAX;
|
|
_max_height = FLT_MIN;
|
|
|
|
PN_stdfloat scalar = _size * _height_scale / image.get_num_channels();
|
|
int pi = 0;
|
|
for (int yi = image.get_y_size() - 1; yi >= 0; --yi) {
|
|
for (int xi = 0; xi < image.get_x_size(); ++xi) {
|
|
LColord rgba = image.get_xel_a(xi, yi);
|
|
PN_stdfloat v = rgba[0] + rgba[1] + rgba[2] + rgba[3];
|
|
v *= scalar;
|
|
_height_data._data[pi] = v;
|
|
++pi;
|
|
_min_height = min(_min_height, v);
|
|
_max_height = max(_max_height, v);
|
|
}
|
|
}
|
|
|
|
compute_slope(0.5f);
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::compute_slope
|
|
// Access: Protected
|
|
// Description: Once _height_data has been filled in, compute the
|
|
// corresponding values for _slope_data.
|
|
////////////////////////////////////////////////////////////////////
|
|
void STBasicTerrain::
|
|
compute_slope(PN_stdfloat smoothing) {
|
|
nassertv(!_height_data._data.empty());
|
|
|
|
int width = _height_data._width;
|
|
int height = _height_data._height;
|
|
_slope_data.reset(width, height);
|
|
|
|
PN_stdfloat u_spacing = _size / (PN_stdfloat)width;
|
|
PN_stdfloat v_spacing = _size / (PN_stdfloat)height;
|
|
|
|
for (int i = 0; i < width; ++i) {
|
|
int left = (i + width - 1) % width;
|
|
int right = (i + 1) % width;
|
|
|
|
for (int j = 0; j < height; ++j) {
|
|
int top = (j + height - 1) % height;
|
|
int bottom = (j + 1) % height;
|
|
|
|
PN_stdfloat slope = 0.0f;
|
|
PN_stdfloat this_height = _height_data._data[i + j * width];
|
|
slope += catan2(cabs(this_height - _height_data._data[right + j * width]), u_spacing);
|
|
slope += catan2(cabs(this_height - _height_data._data[left + j * width]), u_spacing);
|
|
slope += catan2(cabs(this_height - _height_data._data[i + top * width]), v_spacing);
|
|
slope += catan2(cabs(this_height - _height_data._data[i + bottom * width]), v_spacing);
|
|
|
|
slope *= (0.5f / MathNumbers::pi_f);
|
|
|
|
if (slope > 1.0f) {
|
|
slope = 1.0f;
|
|
}
|
|
_slope_data._data[i + j * width] = slope;
|
|
}
|
|
}
|
|
|
|
if (smoothing > 0.0f) {
|
|
// Create a temporary array for smoothing data.
|
|
InterpolationData<PN_stdfloat> smoothing_data;
|
|
smoothing_data.reset(width, height);
|
|
PN_stdfloat *smoothed = &smoothing_data._data[0];
|
|
|
|
int steps = int(smoothing);
|
|
PN_stdfloat last_interpolation = smoothing - steps;
|
|
++steps;
|
|
for (int si = 0; si < steps; ++si) {
|
|
|
|
// compute smoothed normals
|
|
for (int i = 0; i < width; ++i) {
|
|
int left = (i + width - 1) % width;
|
|
int right = (i + 1) % width;
|
|
|
|
for (int j = 0; j < height; ++j) {
|
|
int top = (j + height - 1) % height;
|
|
int bottom = (j + 1) % height;
|
|
|
|
smoothed[i + j * width] = (_slope_data._data[right + j * width] +
|
|
_slope_data._data[left + j * width] +
|
|
_slope_data._data[i + top * width] +
|
|
_slope_data._data[i + bottom * width] +
|
|
_slope_data._data[right + top * width] +
|
|
_slope_data._data[right + bottom * width] +
|
|
_slope_data._data[left + top * width] +
|
|
_slope_data._data[left + bottom * width]);
|
|
smoothed[i + j * width] *= 0.125f;
|
|
}
|
|
}
|
|
|
|
// interpolate or set
|
|
if (si == steps - 1) {
|
|
// last step, interpolate
|
|
for (int i = 0; i < width; ++i) {
|
|
for (int j = 0; j < height; ++j) {
|
|
_slope_data._data[i + j * width] = interpolate(_slope_data._data[i + j * width], smoothed[i + j * width], last_interpolation);
|
|
}
|
|
}
|
|
|
|
} else {
|
|
// full smoothing step, copy everything
|
|
_slope_data = smoothing_data;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: STBasicTerrain::read_quoted_filename
|
|
// Access: Private, Static
|
|
// Description: Reads a quoted filename from the input stream, which
|
|
// is understood to be relative to the indicated
|
|
// directory.
|
|
////////////////////////////////////////////////////////////////////
|
|
void STBasicTerrain::
|
|
read_quoted_filename(Filename &result, istream &in, const Filename &dirname) {
|
|
string filename;
|
|
in >> filename;
|
|
|
|
// The terrain.txt file should, in theory, support spaces, but the
|
|
// SpeedTree reference application doesn't, so we don't bother
|
|
// either.
|
|
if (filename.size() >= 2 && filename[0] == '"' && filename[filename.size() - 1] == '"') {
|
|
filename = filename.substr(1, filename.size() - 2);
|
|
}
|
|
|
|
result = Filename::from_os_specific(filename);
|
|
if (result.is_local()) {
|
|
result = Filename(dirname, result);
|
|
}
|
|
}
|
|
|