diff --git a/panda/src/grutil/config_grutil.cxx b/panda/src/grutil/config_grutil.cxx index ea8aa7174d..5564a3719d 100644 --- a/panda/src/grutil/config_grutil.cxx +++ b/panda/src/grutil/config_grutil.cxx @@ -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(); diff --git a/panda/src/grutil/p3grutil_composite1.cxx b/panda/src/grutil/p3grutil_composite1.cxx index c02bb0c421..85d1c61e50 100644 --- a/panda/src/grutil/p3grutil_composite1.cxx +++ b/panda/src/grutil/p3grutil_composite1.cxx @@ -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" diff --git a/panda/src/grutil/shaderTerrainMesh.I b/panda/src/grutil/shaderTerrainMesh.I new file mode 100644 index 0000000000..1f6e90f03c --- /dev/null +++ b/panda/src/grutil/shaderTerrainMesh.I @@ -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)); +} diff --git a/panda/src/grutil/shaderTerrainMesh.cxx b/panda/src/grutil/shaderTerrainMesh.cxx new file mode 100644 index 0000000000..4a8685ca4c --- /dev/null +++ b/panda/src/grutil/shaderTerrainMesh.cxx @@ -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); +} diff --git a/panda/src/grutil/shaderTerrainMesh.h b/panda/src/grutil/shaderTerrainMesh.h new file mode 100644 index 0000000000..f66caacf76 --- /dev/null +++ b/panda/src/grutil/shaderTerrainMesh.h @@ -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 + +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 diff --git a/samples/shader-terrain/heightfield.png b/samples/shader-terrain/heightfield.png new file mode 100644 index 0000000000..2d72c0445f Binary files /dev/null and b/samples/shader-terrain/heightfield.png differ diff --git a/samples/shader-terrain/main.py b/samples/shader-terrain/main.py new file mode 100644 index 0000000000..10f926fb2f --- /dev/null +++ b/samples/shader-terrain/main.py @@ -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() diff --git a/samples/shader-terrain/models/skybox.bam b/samples/shader-terrain/models/skybox.bam new file mode 100644 index 0000000000..d39bf69577 Binary files /dev/null and b/samples/shader-terrain/models/skybox.bam differ diff --git a/samples/shader-terrain/skybox.frag.glsl b/samples/shader-terrain/skybox.frag.glsl new file mode 100644 index 0000000000..1703cfd425 --- /dev/null +++ b/samples/shader-terrain/skybox.frag.glsl @@ -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); +} diff --git a/samples/shader-terrain/skybox.vert.glsl b/samples/shader-terrain/skybox.vert.glsl new file mode 100644 index 0000000000..464d9d2a94 --- /dev/null +++ b/samples/shader-terrain/skybox.vert.glsl @@ -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; +} diff --git a/samples/shader-terrain/terrain.frag.glsl b/samples/shader-terrain/terrain.frag.glsl new file mode 100644 index 0000000000..0f447842c3 --- /dev/null +++ b/samples/shader-terrain/terrain.frag.glsl @@ -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); +} diff --git a/samples/shader-terrain/terrain.vert.glsl b/samples/shader-terrain/terrain.vert.glsl new file mode 100644 index 0000000000..d693451c89 --- /dev/null +++ b/samples/shader-terrain/terrain.vert.glsl @@ -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; +} diff --git a/samples/shader-terrain/textures/LICENSE.txt b/samples/shader-terrain/textures/LICENSE.txt new file mode 100644 index 0000000000..e59b601333 --- /dev/null +++ b/samples/shader-terrain/textures/LICENSE.txt @@ -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 diff --git a/samples/shader-terrain/textures/grass.png b/samples/shader-terrain/textures/grass.png new file mode 100644 index 0000000000..79e09bb135 Binary files /dev/null and b/samples/shader-terrain/textures/grass.png differ diff --git a/samples/shader-terrain/textures/skybox.jpg b/samples/shader-terrain/textures/skybox.jpg new file mode 100644 index 0000000000..1092bc6aae Binary files /dev/null and b/samples/shader-terrain/textures/skybox.jpg differ