Merge pull request #80 from tobspr/master

Add new terrain implementation (ShaderTerrainMesh)
This commit is contained in:
rdb 2016-04-11 18:35:11 +02:00
commit 7addf3e12b
15 changed files with 1345 additions and 0 deletions

View File

@ -23,6 +23,7 @@
#include "nodeVertexTransform.h"
#include "rigidBodyCombiner.h"
#include "pipeOcclusionCullTraverser.h"
#include "shaderTerrainMesh.h"
#include "dconfig.h"
@ -123,6 +124,7 @@ init_libgrutil() {
RigidBodyCombiner::init_type();
PipeOcclusionCullTraverser::init_type();
SceneGraphAnalyzerMeter::init_type();
ShaderTerrainMesh::init_type();
#ifdef HAVE_AUDIO
MovieTexture::init_type();

View File

@ -1,6 +1,7 @@
#include "cardMaker.cxx"
#include "heightfieldTesselator.cxx"
#include "geoMipTerrain.cxx"
#include "shaderTerrainMesh.cxx"
#include "config_grutil.cxx"
#include "lineSegs.cxx"
#include "fisheyeMaker.cxx"

View File

@ -0,0 +1,191 @@
/**
* 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."
*
* @file shaderTerrainMesh.I
* @author tobspr
* @date 2016-02-16
*/
/**
* @brief Sets the path to the heightfield
* @details This sets the path to the terrain heightfield. It should be 16bit
* single channel, and have a power-of-two resolution greater than 32.
* Common sizes are 2048x2048 or 4096x4096.
*
* @param filename Path to the heightfield
*/
INLINE void ShaderTerrainMesh::set_heightfield_filename(const Filename& filename) {
_heightfield_source = filename;
}
/**
* @brief Returns the heightfield path
* @details This returns the path of the terrain heightfield, previously set with
* set_heightfield()
*
* @return Path to the heightfield
*/
INLINE const Filename& ShaderTerrainMesh::get_heightfield_filename() const {
return _heightfield_source;
}
/**
* @brief Sets the chunk size
* @details This sets the chunk size of the terrain. A chunk is basically the
* smallest unit in LOD. If the chunk size is too small, the terrain will
* perform bad, since there will be way too many chunks. If the chunk size
* is too big, you will not get proper LOD, and might also get bad performance.
*
* For terrains of the size 4096x4096 or 8192x8192, a chunk size of 32 seems
* to produce good results. For smaller resolutions, you should try out a
* size of 16 or even 8 for very small terrains.
*
* The amount of chunks generated for the last level equals to
* (heightfield_size / chunk_size) ** 2. The chunk size has to be a power
* of two.
*
* @param chunk_size Size of the chunks, has to be a power of two
*/
INLINE void ShaderTerrainMesh::set_chunk_size(size_t chunk_size) {
_chunk_size = chunk_size;
}
/**
* @brief Returns the chunk size
* @details This returns the chunk size, previously set with set_chunk_size()
* @return Chunk size
*/
INLINE size_t ShaderTerrainMesh::get_chunk_size() const {
return _chunk_size;
}
/**
* @brief Sets whether to generate patches
* @details If this option is set to true, GeomPatches will be used instead of
* GeomTriangles. This is required when the terrain is used with tesselation
* shaders, since patches are required for tesselation, whereas triangles
* are required for regular rendering.
*
* If this option is set to true while not using a tesselation shader, the
* terrain will not get rendered, or even produce errors. The same applies
* when this is option is not set, but the terrain is used with tesselation
* shaders.
*
* @param generate_patches [description]
*/
INLINE void ShaderTerrainMesh::set_generate_patches(bool generate_patches) {
_generate_patches = generate_patches;
}
/**
* @brief Returns whether to generate patches
* @details This returns whether patches are generated, previously set with
* set_generate_patches()
*
* @return Whether to generate patches
*/
INLINE bool ShaderTerrainMesh::get_generate_patches() const {
return _generate_patches;
}
/**
* @brief Sets the desired triangle width
* @details This sets the desired width a triangle should have in pixels.
* A value of 10.0 for example will make the terrain tesselate everything
* in a way that each triangle edge roughly is 10 pixels wide.
* Of course this will not always accurately match, however you can use this
* setting to control the LOD algorithm of the terrain.
*
* @param target_triangle_width Desired triangle width in pixels
*/
INLINE void ShaderTerrainMesh::set_target_triangle_width(PN_stdfloat target_triangle_width) {
_target_triangle_width = target_triangle_width;
}
/**
* @brief Returns the target triangle width
* @details This returns the target triangle width, previously set with
* ShaderTerrainMesh::set_target_triangle_width()
*
* @return Target triangle width
*/
INLINE PN_stdfloat ShaderTerrainMesh::get_target_triangle_width() const {
return _target_triangle_width;
}
/**
* @brief Sets whether to enable terrain updates
* @details This flag controls whether the terrain should be updated. If this value
* is set to false, no updating of the terrain will happen. This can be useful
* to debug the culling algorithm used by the terrain.
*
* @param update_enabled Whether to update the terrain
*/
INLINE void ShaderTerrainMesh::set_update_enabled(bool update_enabled) {
_update_enabled = update_enabled;
}
/**
* @brief Returns whether the terrain is getting updated
* @details This returns whether the terrain is getting updates, previously set with
* set_update_enabled()
*
* @return Whether to update the terrain
*/
INLINE bool ShaderTerrainMesh::get_update_enabled() const {
return _update_enabled;
}
/**
* @brief Returns a handle to the heightfield texture
* @details This returns a handle to the internally used heightfield texture. This
* can be used to set the heightfield as a shader input.
*
* @return Handle to the heightfield texture
*/
INLINE Texture* ShaderTerrainMesh::get_heightfield_tex() const {
return _heightfield_tex;
}
/**
* @brief Clears all children
* @details This clears all children on the chunk and sets them to NULL. This will
* effectively free all memory consumed by this chunk and its children.
*/
INLINE void ShaderTerrainMesh::Chunk::clear_children() {
for (size_t i = 0; i < 4; ++i) {
delete children[i];
children[i] = NULL;
}
}
/**
* @brief Chunk constructor
* @details This constructs a new chunk, and sets all children to NULL.
*/
INLINE ShaderTerrainMesh::Chunk::Chunk() {
for (size_t i = 0; i < 4; ++i)
children[i] = NULL;
}
/**
* @brief Chunk destructor
* @details This destructs the chunk, freeing all used resources
*/
INLINE ShaderTerrainMesh::Chunk::~Chunk() {
clear_children();
}
/**
* @see ShaderTerrainMesh::uv_to_world(LTexCoord)
*/
INLINE LPoint3 ShaderTerrainMesh::uv_to_world(PN_stdfloat u, PN_stdfloat v) const {
return uv_to_world(LTexCoord(u, v));
}

View File

@ -0,0 +1,715 @@
/**
* 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."
*
* @file shaderTerrainMesh.cxx
* @author tobspr
* @date 2016-02-16
*/
#include "shaderTerrainMesh.h"
#include "geom.h"
#include "geomVertexFormat.h"
#include "geomVertexData.h"
#include "geomVertexWriter.h"
#include "geomNode.h"
#include "geomTriangles.h"
#include "geomPatches.h"
#include "omniBoundingVolume.h"
#include "cullableObject.h"
#include "cullTraverser.h"
#include "cullHandler.h"
#include "cullTraverserData.h"
#include "clockObject.h"
#include "shaderAttrib.h"
#include "renderAttrib.h"
#include "shaderInput.h"
#include "boundingBox.h"
#include "samplerState.h"
#include "config_grutil.h"
#include "typeHandle.h"
ConfigVariableBool stm_use_hexagonal_layout
("stm-use-hexagonal-layout", true,
PRC_DESC("Set this to true to use a hexagonal vertex layout. This approximates "
"the heightfield in a better way, however the CLOD transitions might be "
"visible due to the vertices not matching exactly."));
ConfigVariableInt stm_max_chunk_count
("stm-max-chunk-count", 2048,
PRC_DESC("Controls the maximum amount of chunks the Terrain can display. If you use "
"a high LOD, you might have to increment this value. The lower this value is "
"the less data has to be transferred to the GPU."));
ConfigVariableInt stm_max_views
("stm-max-views", 8,
PRC_DESC("Controls the maximum amount of different views the Terrain can be rendered "
"with. Each camera rendering the terrain corresponds to a view. Lowering this "
"value will reduce the data that has to be transferred to the GPU."));
PStatCollector ShaderTerrainMesh::_basic_collector("Cull:ShaderTerrainMesh:Setup");
PStatCollector ShaderTerrainMesh::_lod_collector("Cull:ShaderTerrainMesh:CollectLOD");
NotifyCategoryDef(shader_terrain, "");
TypeHandle ShaderTerrainMesh::_type_handle;
/**
* @brief Helper function to check for a power of two
* @details This method checks for a power of two by using bitmasks
*
* @param x Number to check
* @return true if x is a power of two, false otherwise
*/
int check_power_of_two(size_t x)
{
return ((x != 0) && ((x & (~x + 1)) == x));
}
/**
* @brief Constructs a new Terrain Mesh
* @details This constructs a new terrain mesh. By default, no transform is set
* on the mesh, causing it to range over the unit box from (0, 0, 0) to
* (1, 1, 1). Usually you want to set a custom transform with NodePath::set_scale()
*/
ShaderTerrainMesh::ShaderTerrainMesh() :
PandaNode("ShaderTerrainMesh"),
_size(0),
_chunk_size(32),
_heightfield_source(""),
_generate_patches(false),
_data_texture(NULL),
_chunk_geom(NULL),
_current_view_index(0),
_last_frame_count(-1),
_target_triangle_width(10.0f),
_update_enabled(true),
_heightfield_tex(NULL)
{
set_final(true);
set_bounds(new OmniBoundingVolume());
}
/**
* @brief Generates the terrain mesh
* @details This generates the terrain mesh, initializing all chunks of the
* internal used quadtree. At this point, a heightfield and a chunk size should
* have been set, otherwise an error is thrown.
*
* If anything goes wrong, like a missing heightfield, then an error is printed
* and false is returned.
*
* @return true if the terrain was initialized, false if an error occured
*/
bool ShaderTerrainMesh::generate() {
if (!do_load_heightfield())
return false;
if (_chunk_size < 8 || !check_power_of_two(_chunk_size)) {
shader_terrain_cat.error() << "Invalid chunk size! Has to be >= 8 and a power of two!" << endl;
return false;
}
if (_chunk_size > _size / 4) {
shader_terrain_cat.error() << "Chunk size too close or greater than the actual terrain size!" << endl;
return false;
}
do_create_chunks();
do_compute_bounds(&_base_chunk);
do_create_chunk_geom();
do_init_data_texture();
do_convert_heightfield();
return true;
}
/**
* @brief Converts the internal used PNMImage to a Texture
* @details This converts the internal used PNMImage to a texture object. The
* reason for this is, that we need the PNMimage for computing the chunk
* bounds, but don't need it afterwards. However, since we have it in ram,
* we can just put its contents into a Texture object, which enables the
* user to call get_heightfield() instead of manually loading the texture
* from disk again to set it as shader input (Panda does not cache PNMImages)
*/
void ShaderTerrainMesh::do_convert_heightfield() {
_heightfield_tex = new Texture();
_heightfield_tex->load(_heightfield);
_heightfield_tex->set_keep_ram_image(true);
if (_heightfield.get_maxval() != 65535) {
shader_terrain_cat.warning() << "Using non 16-bit heightfield!" << endl;
} else {
_heightfield_tex->set_format(Texture::F_r16);
}
_heightfield_tex->set_minfilter(SamplerState::FT_linear);
_heightfield_tex->set_magfilter(SamplerState::FT_linear);
_heightfield.clear();
}
/**
* @brief Intermal method to load the heightfield
* @details This method loads the heightfield from the heightfield path,
* and performs some basic checks, including a check for a power of two,
* and same width and height.
*
* @return true if the heightfield was loaded and meets the requirements
*/
bool ShaderTerrainMesh::do_load_heightfield() {
if(!_heightfield.read(_heightfield_source)) {
shader_terrain_cat.error() << "Could not load heightfield from " << _heightfield_source << endl;
return false;
}
if (_heightfield.get_x_size() != _heightfield.get_y_size()) {
shader_terrain_cat.error() << "Only square heightfields are supported!";
return false;
}
_size = _heightfield.get_x_size();
if (_size < 32 || !check_power_of_two(_size)) {
shader_terrain_cat.error() << "Invalid heightfield! Needs to be >= 32 and a power of two (was: "
<< _size << ")!" << endl;
return false;
}
return true;
}
/**
* @brief Internal method to init the terrain data texture
* @details This method creates the data texture, used to store all chunk data.
* The data texture is set as a shader input later on, and stores the position
* and scale of each chunk. Every row in the data texture denotes a view on
* the terrain.
*/
void ShaderTerrainMesh::do_init_data_texture() {
_data_texture = new Texture("TerrainDataTexture");
_data_texture->setup_2d_texture(stm_max_chunk_count, stm_max_views, Texture::T_float, Texture::F_rgba32);
_data_texture->set_clear_color(LVector4(0));
_data_texture->clear_image();
}
/**
* @brief Internal method to init the quadtree
* @details This method creates the base chunk and then inits all chunks recursively
* by using ShaderTerrainMesh::do_init_chunk().
*/
void ShaderTerrainMesh::do_create_chunks() {
// Release any previously stored children
_base_chunk.clear_children();
// Create the base chunk
_base_chunk.depth = 0;
_base_chunk.x = 0;
_base_chunk.y = 0;
_base_chunk.size = _size;
_base_chunk.edges.set(0, 0, 0, 0);
_base_chunk.avg_height = 0.5;
_base_chunk.min_height = 0.0;
_base_chunk.max_height = 1.0;
_base_chunk.last_clod = 0.0;
do_init_chunk(&_base_chunk);
}
/**
* @brief Internal method to recursively init the quadtree
* @details This method inits the quadtree. Starting from a given node, it
* first examines if that node should be subdivided.
*
* If the node should be subdivided, four children are created and this method
* is called on the children again. If the node is a leaf, all children are
* set to NULL and nothing else happens.
*
* The chunk parameter may not be zero or undefined behaviour occurs.
*
* @param chunk The parent chunk
*/
void ShaderTerrainMesh::do_init_chunk(Chunk* chunk) {
if (chunk->size > _chunk_size) {
// Compute children chunk size
size_t child_chunk_size = chunk->size / 2;
// Subdivide chunk into 4 children
for (size_t y = 0; y < 2; ++y) {
for (size_t x = 0; x < 2; ++x) {
Chunk* child = new Chunk();
child->size = child_chunk_size;
child->depth = chunk->depth + 1;
child->x = chunk->x + x * child_chunk_size;
child->y = chunk->y + y * child_chunk_size;
do_init_chunk(child);
chunk->children[x + 2*y] = child;
}
}
} else {
// Final chunk, initialize all children to zero
for (size_t i = 0; i < 4; ++i) {
chunk->children[i] = NULL;
}
}
}
/**
* @brief Recursively computes the bounds for a given chunk
* @details This method takes a parent chunk, and computes the bounds recursively,
* depending on whether the chunk is a leaf or a node.
*
* If the chunk is a leaf, then the average, min and max values for that chunk
* are computed by iterating over the heightfield region of that chunk.
*
* If the chunk is a node, this method is called recursively on all children
* first, and after that, the average, min and max values for that chunk
* are computed by merging those values of the children.
*
* If chunk is NULL, undefined behaviour occurs.
*
* @param chunk The parent chunk
*/
void ShaderTerrainMesh::do_compute_bounds(Chunk* chunk) {
// Final chunk (Leaf)
if (chunk->size == _chunk_size) {
// Get a pointer to the PNMImage data, this is faster than using get_xel()
// for all pixels, since get_xel() also includes bounds checks and so on.
xel* data = _heightfield.get_array();
// Pixel getter function. Note that we have to flip the Y-component, since
// panda itself also flips it
// auto get_xel = [&](size_t x, size_t y){ return data[x + (_size - 1 - y) * _size].b / (PN_stdfloat)PGM_MAXMAXVAL; };
#define get_xel(x, y) (data[(x) + (_size - 1 - (y)) * _size].b / (PN_stdfloat)PGM_MAXMAXVAL)
// Iterate over all pixels
PN_stdfloat avg_height = 0.0, min_height = 1.0, max_height = 0.0;
for (size_t x = 0; x < _chunk_size; ++x) {
for (size_t y = 0; y < _chunk_size; ++y) {
// Access data directly, to improve performance
PN_stdfloat height = get_xel(chunk->x + x, chunk->y + y);
avg_height += height;
min_height = min(min_height, height);
max_height = max(max_height, height);
}
}
// Normalize average height
avg_height /= _chunk_size * _chunk_size;
// Store values
chunk->min_height = min_height;
chunk->max_height = max_height;
chunk->avg_height = avg_height;
// Get edges in the order (0, 0) (1, 0) (0, 1) (1, 1)
for (size_t y = 0; y < 2; ++y) {
for (size_t x = 0; x < 2; ++x) {
chunk->edges.set_cell(x + 2 * y, get_xel(
chunk->x + x * (_chunk_size - 1),
chunk->y + y * (_chunk_size - 1)
));
}
}
#undef get_xel
} else {
// Reset heights
chunk->avg_height = 0.0;
chunk->min_height = 1.0;
chunk->max_height = 0.0;
// Perform bounds computation for every children and merge the children values
for (size_t i = 0; i < 4; ++i) {
do_compute_bounds(chunk->children[i]);
chunk->avg_height += chunk->children[i]->avg_height / 4.0;
chunk->min_height = min(chunk->min_height, chunk->children[i]->min_height);
chunk->max_height = max(chunk->max_height, chunk->children[i]->max_height);
}
// Also take the edge points from the children
chunk->edges.set_x(chunk->children[0]->edges.get_x());
chunk->edges.set_y(chunk->children[1]->edges.get_y());
chunk->edges.set_z(chunk->children[2]->edges.get_z());
chunk->edges.set_w(chunk->children[3]->edges.get_w());
}
}
/**
* @brief Internal method to create the chunk geom
* @details This method generates the internal used base chunk. The base chunk geom
* is used to render the actual terrain, and will get instanced for every chunk.
*
* The chunk has a size of (size+3) * (size+3), since additional triangles are
* inserted at the borders to prevent holes between chunks of a different LOD.
*
* If the generate patches option is set, patches will be generated instead
* of triangles, which allows the terrain to use a tesselation shader.
*/
void ShaderTerrainMesh::do_create_chunk_geom() {
// Convert chunk size to an integer, because we operate on integers and get
// signed/unsigned mismatches otherwise
int size = (int)_chunk_size;
// Create vertex data
PT(GeomVertexData) gvd = new GeomVertexData("vertices", GeomVertexFormat::get_v3(), Geom::UH_static);
gvd->reserve_num_rows( (size + 3) * (size + 3) );
GeomVertexWriter vertex_writer(gvd, "vertex");
// Create primitive
PT(GeomPrimitive) triangles = NULL;
if (_generate_patches) {
triangles = new GeomPatches(3, Geom::UH_static);
} else {
triangles = new GeomTriangles(Geom::UH_static);
}
// Insert chunk vertices
for (int y = -1; y <= size + 1; ++y) {
for (int x = -1; x <= size + 1; ++x) {
LVector3 vtx_pos(x / (PN_stdfloat)size, y / (PN_stdfloat)size, 0.0f);
// Stitched vertices at the cornders
if (x == -1 || y == -1 || x == size + 1 || y == size + 1) {
vtx_pos.set_z(-1.0f / (PN_stdfloat)size);
vtx_pos.set_x(max(0.0f, min(1.0f, vtx_pos.get_x())));
vtx_pos.set_y(max(0.0f, min(1.0f, vtx_pos.get_y())));
}
vertex_writer.add_data3f(vtx_pos);
}
}
// Its important to use int and not size_t here, since we do store negative values
// auto get_point_index = [&size](int x, int y){ return (x + 1) + (size + 3) * (y + 1); };
#define get_point_index(x, y) (((x) + 1) + (size + 3) * ((y) + 1))
// Create triangles
for (int y = -1; y <= size; ++y) {
for (int x = -1; x <= size; ++x) {
// Get point indices of the quad vertices
int tl = get_point_index(x, y);
int tr = get_point_index(x + 1, y);
int bl = get_point_index(x, y + 1);
int br = get_point_index(x + 1, y + 1);
// Vary triangle scheme on each uneven quad
if (stm_use_hexagonal_layout && (x + y) % 2 == 0 ) {
triangles->add_vertices(tl, tr, bl);
triangles->add_vertices(bl, tr, br);
} else {
triangles->add_vertices(tl, tr, br);
triangles->add_vertices(tl, br, bl);
}
}
}
#undef get_point_index
// Construct geom
PT(Geom) geom = new Geom(gvd);
geom->add_primitive(triangles);
// Do not set any bounds, we do culling ourself
geom->clear_bounds();
geom->set_bounds(new OmniBoundingVolume());
_chunk_geom = geom;
}
/**
* @copydoc PandaNode::is_renderable()
*/
bool ShaderTerrainMesh::is_renderable() const {
return true;
}
/**
* @copydoc PandaNode::is_renderable()
*/
bool ShaderTerrainMesh::safe_to_flatten() const {
return false;
}
/**
* @copydoc PandaNode::safe_to_combine()
*/
bool ShaderTerrainMesh::safe_to_combine() const {
return false;
}
/**
* @copydoc PandaNode::add_for_draw()
*/
void ShaderTerrainMesh::add_for_draw(CullTraverser *trav, CullTraverserData &data) {
// Make sure the terrain was properly initialized, and the geom was created
// successfully
nassertv(_data_texture != NULL);
nassertv(_chunk_geom != NULL);
_basic_collector.start();
// Get current frame count
int frame_count = ClockObject::get_global_clock()->get_frame_count();
if (_last_frame_count != frame_count) {
// Frame count changed, this means we are at the beginning of a new frame.
// In this case, update the frame count and reset the view index.
_last_frame_count = frame_count;
_current_view_index = 0;
}
// Get transform and render state for this render pass
CPT(TransformState) modelview_transform = data.get_internal_transform(trav);
CPT(RenderState) state = data._state->compose(get_state());
// Store a handle to the scene setup
const SceneSetup* scene = trav->get_scene();
// Get the MVP matrix, this is required for the LOD
const Lens* current_lens = scene->get_lens();
const LMatrix4& projection_mat = current_lens->get_projection_mat();
// Get the current lens bounds
PT(BoundingVolume) cam_bounds = scene->get_cull_bounds();
// Transform the camera bounds with the main camera transform
DCAST(GeometricBoundingVolume, cam_bounds)->xform(scene->get_camera_transform()->get_mat());
TraversalData traversal_data;
traversal_data.cam_bounds = cam_bounds;
traversal_data.model_mat = get_transform()->get_mat();
traversal_data.mvp_mat = modelview_transform->get_mat() * projection_mat;
traversal_data.emitted_chunks = 0;
traversal_data.storage_ptr = (ChunkDataEntry*)_data_texture->modify_ram_image().p();
traversal_data.screen_size.set(scene->get_viewport_width(), scene->get_viewport_height());
// Move write pointer so it points to the beginning of the current view
traversal_data.storage_ptr += _data_texture->get_x_size() * _current_view_index;
if (_update_enabled) {
// Traverse recursively
_lod_collector.start();
do_traverse(&_base_chunk, &traversal_data);
_lod_collector.stop();
} else {
// Do a rough guess of the emitted chunks, we don't know the actual count
// (we would have to store it). This is only for debugging anyways, so
// its not important we get an accurate count here.
traversal_data.emitted_chunks = _data_texture->get_x_size();
}
// Set shader inputs
CPT(RenderAttrib) current_shader_attrib = state->get_attrib_def(ShaderAttrib::get_class_slot());
// Make sure the user didn't forget to set a shader
if (!DCAST(ShaderAttrib, current_shader_attrib)->has_shader()) {
shader_terrain_cat.warning() << "No shader set on the terrain! You need to set the appropriate shader!" << endl;
}
// Should never happen
nassertv(current_shader_attrib != NULL);
current_shader_attrib = DCAST(ShaderAttrib, current_shader_attrib)->set_shader_input(
new ShaderInput("ShaderTerrainMesh.terrain_size", LVecBase2i(_size)) );
current_shader_attrib = DCAST(ShaderAttrib, current_shader_attrib)->set_shader_input(
new ShaderInput("ShaderTerrainMesh.chunk_size", LVecBase2i(_chunk_size)));
current_shader_attrib = DCAST(ShaderAttrib, current_shader_attrib)->set_shader_input(
new ShaderInput("ShaderTerrainMesh.view_index", LVecBase2i(_current_view_index)));
current_shader_attrib = DCAST(ShaderAttrib, current_shader_attrib)->set_shader_input(
new ShaderInput("ShaderTerrainMesh.data_texture", _data_texture));
current_shader_attrib = DCAST(ShaderAttrib, current_shader_attrib)->set_shader_input(
new ShaderInput("ShaderTerrainMesh.heightfield", _heightfield_tex));
current_shader_attrib = DCAST(ShaderAttrib, current_shader_attrib)->set_instance_count(
traversal_data.emitted_chunks);
state = state->set_attrib(current_shader_attrib, 10000);
// Emit chunk
CullableObject *object = new CullableObject(_chunk_geom, state, modelview_transform);
trav->get_cull_handler()->record_object(object, trav);
// After rendering, increment the view index
++_current_view_index;
if (_current_view_index > stm_max_views) {
shader_terrain_cat.error() << "More views than supported! Increase the stm-max-views config variable!" << endl;
}
_basic_collector.stop();
}
/**
* @brief Traverses the quadtree
* @details This method traverses the given chunk, deciding whether it should
* be rendered or subdivided.
*
* In case the chunk is decided to be subdivided, this method is called on
* all children.
*
* In case the chunk is decided to be rendered, ShaderTerrainMesh::do_emit_chunk() is
* called. Otherwise nothing happens, and the chunk does not get rendered.
*
* @param chunk Chunk to traverse
* @param data Traversal data
*/
void ShaderTerrainMesh::do_traverse(Chunk* chunk, TraversalData* data, bool fully_visible) {
// Don't check bounds if we are fully visible
if (!fully_visible) {
// Construct chunk bounding volume
PN_stdfloat scale = 1.0 / (PN_stdfloat)_size;
LPoint3 bb_min(chunk->x * scale, chunk->y * scale, chunk->min_height);
LPoint3 bb_max((chunk->x + chunk->size) * scale, (chunk->y + chunk->size) * scale, chunk->max_height);
BoundingBox bbox = BoundingBox(bb_min, bb_max);
DCAST(GeometricBoundingVolume, &bbox)->xform(data->model_mat);
int intersection = data->cam_bounds->contains(&bbox);
if (intersection == BoundingVolume::IF_no_intersection) {
// No intersection with frustum
return;
}
// If the bounds are fully visible, there is no reason to perform culling
// on the children, so we set this flag to prevent any bounding computation
// on the child nodes.
fully_visible = (intersection & BoundingVolume::IF_all) != 0;
}
// Check if the chunk should be subdivided. In case the chunk is a leaf node,
// the chunk will never get subdivided.
// NOTE: We still always perform the LOD check. This is for the reason that
// the lod check also computes the CLOD factor, which is useful.
if (do_check_lod_matches(chunk, data) || chunk->size == _chunk_size) {
do_emit_chunk(chunk, data);
} else {
// Traverse children
for (size_t i = 0; i < 4; ++i) {
do_traverse(chunk->children[i], data, fully_visible);
}
}
}
/**
* @brief Checks whether a chunk should get subdivided
* @details This method checks whether a chunk fits on screen, or should be
* subdivided in order to provide bigger detail.
*
* In case this method returns true, the chunk lod is fine, and the chunk
* can be rendered. If the method returns false, the chunk should be subdivided.
*
* @param chunk Chunk to check
* @param data Traversal data
*
* @return true if the chunk is sufficient, false if the chunk should be subdivided
*/
bool ShaderTerrainMesh::do_check_lod_matches(Chunk* chunk, TraversalData* data) {
// Project all points to world space
LVector2 projected_points[4];
for (size_t y = 0; y < 2; ++y) {
for (size_t x = 0; x < 2; ++x) {
// Compute point in model space (0,0,0 to 1,1,1)
LVector3 edge_pos = LVector3(
(PN_stdfloat)(chunk->x + x * (chunk->size - 1)) / (PN_stdfloat)_size,
(PN_stdfloat)(chunk->y + y * (chunk->size - 1)) / (PN_stdfloat)_size,
chunk->edges.get_cell(x + 2 * y)
);
LVector4 projected = data->mvp_mat.xform(LVector4(edge_pos, 1.0));
if (projected.get_w() == 0.0) {
projected.set(0.0, 0.0, -1.0, 1.0f);
}
projected *= 1.0 / projected.get_w();
projected_points[x + 2 * y].set(
projected.get_x() * data->screen_size.get_x(),
projected.get_y() * data->screen_size.get_y());
}
}
// Compute the length of the edges in screen space
PN_stdfloat edge_top = (projected_points[1] - projected_points[3]).length_squared();
PN_stdfloat edge_right = (projected_points[0] - projected_points[2]).length_squared();
PN_stdfloat edge_bottom = (projected_points[2] - projected_points[3]).length_squared();
PN_stdfloat edge_left = (projected_points[0] - projected_points[1]).length_squared();
// CLOD factor
PN_stdfloat max_edge = max(edge_top, max(edge_right, max(edge_bottom, edge_left)));
// Micro-Optimization: We use length_squared() instead of length() to compute the
// maximum edge length. This reduces it to one csqrt instead of four.
max_edge = csqrt(max_edge);
PN_stdfloat tesselation_factor = (max_edge / _target_triangle_width) / (PN_stdfloat)_chunk_size;
PN_stdfloat clod_factor = max(0.0, min(1.0, 2.0 - tesselation_factor));
// Store the clod factor
chunk->last_clod = clod_factor;
return tesselation_factor <= 2.0;
}
/**
* @brief Internal method to spawn a chunk
* @details This method is used to spawn a chunk in case the traversal decided
* that the chunk gets rendered. It writes the chunks data to the texture, and
* increments the write pointer
*
* @param chunk Chunk to spawn
* @param data Traversal data
*/
void ShaderTerrainMesh::do_emit_chunk(Chunk* chunk, TraversalData* data) {
if (data->emitted_chunks >= _data_texture->get_x_size()) {
// Only print warning once
if (data->emitted_chunks == _data_texture->get_x_size()) {
shader_terrain_cat.error() << "Too many chunks in the terrain! Consider lowering the desired LOD, or increase the stm-max-chunk-count variable." << endl;
data->emitted_chunks++;
}
return;
}
ChunkDataEntry& data_entry = *data->storage_ptr;
data_entry.x = chunk->x;
data_entry.y = chunk->y;
data_entry.size = chunk->size / _chunk_size;
data_entry.clod = chunk->last_clod;
data->emitted_chunks ++;
data->storage_ptr ++;
}
/**
* @brief Transforms a texture coordinate to world space
* @details This transforms a texture coordinatefrom uv-space (0 to 1) to world
* space. This takes the terrains transform into account, and also samples the
* heightmap. This method should be called after generate().
*
* @param coord Coordinate in uv-space from 0, 0 to 1, 1
* @return World-Space point
*/
LPoint3 ShaderTerrainMesh::uv_to_world(const LTexCoord& coord) const {
nassertr(_heightfield_tex != NULL, LPoint3(0));
PT(TexturePeeker) peeker = _heightfield_tex->peek();
nassertr(peeker != NULL, LPoint3(0));
LColor result;
if (!peeker->lookup_bilinear(result, coord.get_x(), coord.get_y())) {
shader_terrain_cat.error() << "UV out of range, cant transform to world!" << endl;
return LPoint3(0);
}
LPoint3 unit_point(coord.get_x(), coord.get_y(), result.get_x());
return get_transform()->get_mat().xform_point_general(unit_point);
}

View File

@ -0,0 +1,205 @@
/**
* 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."
*
* @file shaderTerrainMesh.h
* @author tobspr
* @date 2016-02-16
*/
#ifndef SHADER_TERRAIN_MESH_H
#define SHADER_TERRAIN_MESH_H
#include "pandabase.h"
#include "luse.h"
#include "pnmImage.h"
#include "geom.h"
#include "pandaNode.h"
#include "texture.h"
#include "texturePeeker.h"
#include "configVariableBool.h"
#include "configVariableInt.h"
#include "pStatCollector.h"
#include "filename.h"
#include <stdint.h>
extern ConfigVariableBool stm_use_hexagonal_layout;
extern ConfigVariableInt stm_max_chunk_count;
extern ConfigVariableInt stm_max_views;
NotifyCategoryDecl(shader_terrain, EXPCL_PANDA_GRUTIL, EXPTP_PANDA_GRUTIL);
/**
* @brief Terrain Renderer class utilizing the GPU
* @details This class provides functionality to render heightfields of large
* sizes utilizing the GPU. Internally a quadtree is used to generate the LODs.
* The final terrain is then rendered using instancing on the GPU. This makes
* it possible to use very large heightfields (8192+) with very reasonable
* performance. The terrain provides options to control the LOD using a
* target triangle width, see ShaderTerrainMesh::set_target_triangle_width().
*
* Because the Terrain is rendered entirely on the GPU, it needs a special
* vertex shader. There is a default vertex shader available, which you can
* use in your own shaders. IMPORTANT: If you don't set an appropriate shader
* on the terrain, nothing will be visible.
*/
class EXPCL_PANDA_GRUTIL ShaderTerrainMesh : public PandaNode {
PUBLISHED:
ShaderTerrainMesh();
INLINE void set_heightfield_filename(const Filename& filename);
INLINE const Filename& get_heightfield_filename() const;
MAKE_PROPERTY(heightfield_filename, get_heightfield_filename, set_heightfield_filename);
INLINE void set_chunk_size(size_t chunk_size);
INLINE size_t get_chunk_size() const;
MAKE_PROPERTY(chunk_size, get_chunk_size, set_chunk_size);
INLINE void set_generate_patches(bool generate_patches);
INLINE bool get_generate_patches() const;
MAKE_PROPERTY(generate_patches, get_generate_patches, set_generate_patches);
INLINE void set_update_enabled(bool update_enabled);
INLINE bool get_update_enabled() const;
MAKE_PROPERTY(update_enabled, get_update_enabled, set_update_enabled);
INLINE void set_target_triangle_width(PN_stdfloat target_triangle_width);
INLINE PN_stdfloat get_target_triangle_width() const;
MAKE_PROPERTY(target_triangle_width, get_target_triangle_width, set_target_triangle_width);
INLINE Texture* get_heightfield_tex() const;
MAKE_PROPERTY(heightfield_tex, get_heightfield_tex);
LPoint3 uv_to_world(const LTexCoord& coord) const;
INLINE LPoint3 uv_to_world(PN_stdfloat u, PN_stdfloat v) const;
bool generate();
public:
// Methods derived from PandaNode
virtual bool is_renderable() const;
virtual bool safe_to_flatten() const;
virtual bool safe_to_combine() const;
virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data);
private:
// Chunk data
struct Chunk {
// Depth, starting at 0
size_t depth;
// Chunk position in heightfield space
size_t x, y;
// Chunk size in heightfield space
size_t size;
// Children, in the order (0, 0) (1, 0) (0, 1) (1, 1)
Chunk* children[4];
// Chunk heights, used for culling
PN_stdfloat avg_height, min_height, max_height;
// Edge heights, used for lod computation, in the same order as the children
LVector4 edges;
// Last CLOD factor, stored while computing LOD, used for seamless transitions between lods
PN_stdfloat last_clod;
INLINE void clear_children();
INLINE Chunk();
INLINE ~Chunk();
};
// Single entry in the data block
struct ChunkDataEntry {
// float x, y, size, clod;
// Panda uses BGRA, the above layout shows how its actually in texture memory,
// the layout below makes it work with BGRA.
PN_float32 size, y, x, clod;
};
// Data used while traversing all chunks
struct TraversalData {
// Global MVP used for LOD
LMatrix4 mvp_mat;
// Local model matrix used for culling
LMatrix4 model_mat;
// Camera bounds in world space
BoundingVolume* cam_bounds;
// Amount of emitted chunks so far
int emitted_chunks;
// Screen resolution, used for LOD
LVector2i screen_size;
// Pointer to the texture memory, where each chunk is written to
ChunkDataEntry* storage_ptr;
};
bool do_load_heightfield();
void do_convert_heightfield();
void do_init_data_texture();
void do_create_chunks();
void do_init_chunk(Chunk* chunk);
void do_compute_bounds(Chunk* chunk);
void do_create_chunk_geom();
void do_traverse(Chunk* chunk, TraversalData* data, bool fully_visible = false);
void do_emit_chunk(Chunk* chunk, TraversalData* data);
bool do_check_lod_matches(Chunk* chunk, TraversalData* data);
Chunk _base_chunk;
Filename _heightfield_source;
size_t _size;
size_t _chunk_size;
bool _generate_patches;
PNMImage _heightfield;
PT(Texture) _heightfield_tex;
PT(Geom) _chunk_geom;
PT(Texture) _data_texture;
size_t _current_view_index;
int _last_frame_count;
PN_stdfloat _target_triangle_width;
bool _update_enabled;
// PStats stuff
static PStatCollector _lod_collector;
static PStatCollector _basic_collector;
// Type handle stuff
public:
static TypeHandle get_class_type() {
return _type_handle;
}
static void init_type() {
PandaNode::init_type();
register_type(_type_handle, "ShaderTerrainMesh", PandaNode::get_class_type());
}
virtual TypeHandle get_type() const {
return get_class_type();
}
virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
private:
static TypeHandle _type_handle;
};
#include "shaderTerrainMesh.I"
#endif // SHADER_TERRAIN_MESH_H

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -0,0 +1,80 @@
#!/usr/bin/env python
# Author: tobspr
#
# Last Updated: 2016-02-13
#
# This tutorial provides an example of using the ShaderTerrainMesh class
import os, sys, math, random
from direct.showbase.ShowBase import ShowBase
from panda3d.core import ShaderTerrainMesh, Shader, load_prc_file_data
from panda3d.core import SamplerState
class ShaderTerrainDemo(ShowBase):
def __init__(self):
# Load some configuration variables, its important for this to happen
# before the ShowBase is initialized
load_prc_file_data("", """
textures-power-2 none
window-title Panda3D Shader Terrain Demo
""")
# Initialize the showbase
ShowBase.__init__(self)
# Increase camera FOV aswell as the far plane
self.camLens.set_fov(90)
self.camLens.set_near_far(0.1, 50000)
# Construct the terrain
self.terrain_node = ShaderTerrainMesh()
# Set a heightfield, the heightfield should be a 16-bit png and
# have a quadratic size of a power of two.
self.terrain_node.heightfield_filename = "heightfield.png"
# Set the target triangle width. For a value of 10.0 for example,
# the terrain will attempt to make every triangle 10 pixels wide on screen.
self.terrain_node.target_triangle_width = 10.0
# Generate the terrain
self.terrain_node.generate()
# Attach the terrain to the main scene and set its scale
self.terrain = self.render.attach_new_node(self.terrain_node)
self.terrain.set_scale(1024, 1024, 100)
self.terrain.set_pos(-512, -512, -70.0)
# Set a shader on the terrain. The ShaderTerrainMesh only works with
# an applied shader. You can use the shaders used here in your own shaders
terrain_shader = Shader.load(Shader.SL_GLSL, "terrain.vert.glsl", "terrain.frag.glsl")
self.terrain.set_shader(terrain_shader)
self.terrain.set_shader_input("camera", self.camera)
# Set some texture on the terrain
grass_tex = self.loader.loadTexture("textures/grass.png")
grass_tex.set_minfilter(SamplerState.FT_linear_mipmap_linear)
grass_tex.set_anisotropic_degree(16)
self.terrain.set_texture(grass_tex)
# Load some skybox - you can safely ignore this code
skybox = self.loader.loadModel("models/skybox.bam")
skybox.reparent_to(self.render)
skybox.set_scale(20000)
skybox_texture = self.loader.loadTexture("textures/skybox.jpg")
skybox_texture.set_minfilter(SamplerState.FT_linear)
skybox_texture.set_magfilter(SamplerState.FT_linear)
skybox_texture.set_wrap_u(SamplerState.WM_repeat)
skybox_texture.set_wrap_v(SamplerState.WM_mirror)
skybox_texture.set_anisotropic_degree(16)
skybox.set_texture(skybox_texture)
skybox_shader = Shader.load(Shader.SL_GLSL, "skybox.vert.glsl", "skybox.frag.glsl")
skybox.set_shader(skybox_shader)
demo = ShaderTerrainDemo()
demo.run()

Binary file not shown.

View File

@ -0,0 +1,21 @@
#version 150
in vec3 skybox_pos;
out vec4 color;
uniform sampler2D p3d_Texture0;
void main() {
vec3 view_dir = normalize(skybox_pos);
vec2 skybox_uv;
// Convert spherical coordinates
const float pi = 3.14159265359;
skybox_uv.x = (atan(view_dir.y, view_dir.x) + (0.5 * pi)) / (2 * pi);
skybox_uv.y = clamp(view_dir.z * 0.72 + 0.35, 0.0, 1.0);
vec3 skybox_color = textureLod(p3d_Texture0, skybox_uv, 0).xyz;
color = vec4(skybox_color, 1);
}

View File

@ -0,0 +1,13 @@
#version 150
// This is just a simple vertex shader transforming the skybox
in vec4 p3d_Vertex;
uniform mat4 p3d_ModelViewProjectionMatrix;
out vec3 skybox_pos;
void main() {
skybox_pos = p3d_Vertex.xyz;
gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
}

View File

@ -0,0 +1,56 @@
#version 150
// This is the terrain fragment shader. There is a lot of code in here
// which is not necessary to render the terrain, but included for convenience -
// Like generating normals from the heightmap or a simple fog effect.
// Most of the time you want to adjust this shader to get your terrain the look
// you want. The vertex shader most likely will stay the same.
in vec2 terrain_uv;
in vec3 vtx_pos;
out vec4 color;
uniform struct {
sampler2D data_texture;
sampler2D heightfield;
int view_index;
int terrain_size;
int chunk_size;
} ShaderTerrainMesh;
uniform sampler2D p3d_Texture0;
uniform vec3 wspos_camera;
// Compute normal from the heightmap, this assumes the terrain is facing z-up
vec3 get_terrain_normal() {
const float terrain_height = 50.0;
vec3 pixel_size = vec3(1.0, -1.0, 0) / textureSize(ShaderTerrainMesh.heightfield, 0).xxx;
float u0 = texture(ShaderTerrainMesh.heightfield, terrain_uv + pixel_size.yz).x * terrain_height;
float u1 = texture(ShaderTerrainMesh.heightfield, terrain_uv + pixel_size.xz).x * terrain_height;
float v0 = texture(ShaderTerrainMesh.heightfield, terrain_uv + pixel_size.zy).x * terrain_height;
float v1 = texture(ShaderTerrainMesh.heightfield, terrain_uv + pixel_size.zx).x * terrain_height;
vec3 tangent = normalize(vec3(1.0, 0, u1 - u0));
vec3 binormal = normalize(vec3(0, 1.0, v1 - v0));
return normalize(cross(tangent, binormal));
}
void main() {
vec3 diffuse = texture(p3d_Texture0, terrain_uv * 16.0).xyz;
vec3 normal = get_terrain_normal();
// Add some fake lighting - you usually want to use your own lighting code here
vec3 fake_sun = normalize(vec3(0.7, 0.2, 0.6));
vec3 shading = max(0.0, dot(normal, fake_sun)) * diffuse;
shading += vec3(0.07, 0.07, 0.1);
// Fake fog
float dist = distance(vtx_pos, wspos_camera);
float fog_factor = smoothstep(0, 1, dist / 1000.0);
shading = mix(shading, vec3(0.7, 0.7, 0.8), fog_factor);
color = vec4(shading, 1.0);
}

View File

@ -0,0 +1,56 @@
#version 150
// This is the default terrain vertex shader. Most of the time you can just copy
// this and reuse it, and just modify the fragment shader.
in vec4 p3d_Vertex;
uniform mat4 p3d_ModelViewProjectionMatrix;
uniform mat4 p3d_ModelMatrix;
uniform struct {
sampler2D data_texture;
sampler2D heightfield;
int view_index;
int terrain_size;
int chunk_size;
} ShaderTerrainMesh;
out vec2 terrain_uv;
out vec3 vtx_pos;
void main() {
// Terrain data has the layout:
// x: x-pos, y: y-pos, z: size, w: clod
vec4 terrain_data = texelFetch(ShaderTerrainMesh.data_texture,
ivec2(gl_InstanceID, ShaderTerrainMesh.view_index), 0);
// Get initial chunk position in the (0, 0, 0), (1, 1, 0) range
vec3 chunk_position = p3d_Vertex.xyz;
// CLOD implementation
float clod_factor = smoothstep(0, 1, terrain_data.w);
chunk_position.xy -= clod_factor * fract(chunk_position.xy * ShaderTerrainMesh.chunk_size / 2.0)
* 2.0 / ShaderTerrainMesh.chunk_size;
// Scale the chunk
chunk_position *= terrain_data.z * float(ShaderTerrainMesh.chunk_size)
/ float(ShaderTerrainMesh.terrain_size);
chunk_position.z *= ShaderTerrainMesh.chunk_size;
// Offset the chunk, it is important that this happens after the scale
chunk_position.xy += terrain_data.xy / float(ShaderTerrainMesh.terrain_size);
// Compute the terrain UV coordinates
terrain_uv = chunk_position.xy;
// Sample the heightfield and offset the terrain - we do not need to multiply
// the height with anything since the terrain transform is included in the
// model view projection matrix.
chunk_position.z += texture(ShaderTerrainMesh.heightfield, terrain_uv).x;
gl_Position = p3d_ModelViewProjectionMatrix * vec4(chunk_position, 1);
// Output the vertex world space position - in this case we use this to render
// the fog.
vtx_pos = (p3d_ModelMatrix * vec4(chunk_position, 1)).xyz;
}

View File

@ -0,0 +1,5 @@
Grass texture from (cc by 3.0)
http://opengameart.org/content/grass-texture
Skybox by rdb
http://rdb.name/PANO_20140818_112419.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB