/* * Copyright (C) 2021 Christopher J. Howard * * This file is part of Antkeeper source code. * * Antkeeper source code is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Antkeeper source code is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Antkeeper source code. If not, see . */ #include "entity/systems/terrain.hpp" #include "entity/components/celestial-body.hpp" #include "entity/components/observer.hpp" #include "entity/components/terrain.hpp" #include "geom/meshes/grid.hpp" #include "geom/mesh-functions.hpp" #include "geom/morton.hpp" #include "geom/quadtree.hpp" #include "geom/spherical.hpp" #include "gl/vertex-attribute-type.hpp" #include "math/constants.hpp" #include "math/quaternion-operators.hpp" #include "renderer/vertex-attributes.hpp" #include "utility/fundamental-types.hpp" #include #include namespace entity { namespace system { terrain::terrain(entity::registry& registry): updatable(registry), patch_subdivisions(0), patch_base_mesh(nullptr), patch_vertex_size(0), patch_vertex_count(0), patch_vertex_data(nullptr), patch_scene_collection(nullptr), max_error(0.0) { // Build set of quaternions to rotate quadtree cube coordinates into BCBF space according to face index face_rotations[0] = math::quaternion::identity(); // +x face_rotations[1] = math::quaternion::rotate_z(math::pi); // -x face_rotations[2] = math::quaternion::rotate_z( math::half_pi); // +y face_rotations[3] = math::quaternion::rotate_z(-math::half_pi); // -y face_rotations[4] = math::quaternion::rotate_y(-math::half_pi); // +z face_rotations[5] = math::quaternion::rotate_y( math::half_pi); // -z // Specify vertex size and stride // (position + uv + normal + tangent + barycentric + target) patch_vertex_size = 3 + 2 + 3 + 4 + 3 + 3; patch_vertex_stride = patch_vertex_size * sizeof(float); // Init patch subdivisions to zero set_patch_subdivisions(0); registry.on_construct().connect<&terrain::on_terrain_construct>(this); registry.on_destroy().connect<&terrain::on_terrain_destroy>(this); } terrain::~terrain() {} void terrain::update(double t, double dt) { // Refine the level of detail of each terrain quadsphere registry.view().each( [&](entity::id terrain_eid, const auto& terrain_component, const auto& terrain_body) { // Retrieve terrain quadsphere terrain_quadsphere* quadsphere = terrain_quadspheres[terrain_eid]; // For each observer this->registry.view().each( [&](entity::id observer_eid, const auto& observer) { // Skip observers with invalid reference body if (!this->registry.has(observer.reference_body_eid) || !this->registry.has(observer.reference_body_eid)) return; // Get celestial body and terrain component of observer reference body const auto& reference_celestial_body = this->registry.get(observer.reference_body_eid); const auto& reference_terrain = this->registry.get(observer.reference_body_eid); // Calculate reference BCBF-space position of observer. //double3 observer_spherical = {reference_celestial_body.radius + observer.elevation, observer.latitude, observer.longitude}; double3 observer_spherical = {observer.elevation, observer.latitude, observer.longitude}; double3 observer_cartesian = geom::spherical::to_cartesian(observer_spherical); observer_cartesian = math::type_cast(observer.camera->get_translation()); /// @TODO Transform observer position into BCBF space of terrain body (use orbit component?) // For each terrain quadsphere face for (int i = 0; i < 6; ++i) { terrain_quadsphere_face& quadsphere_face = quadsphere->faces[i]; // Get the quadsphere faces's quadtree quadtree_type& quadtree = quadsphere_face.quadtree; // Clear quadsphere face quadtree quadtree.clear(); // For each node in the face quadtree for (auto node_it = quadtree.begin(); node_it != quadtree.end(); ++node_it) { quadtree_node_type node = *node_it; // Skip non leaf nodes if (!quadtree.is_leaf(node)) continue; // Extract node depth quadtree_type::node_type node_depth = quadtree_type::depth(node); // Skip nodes at max depth level if (node_depth >= terrain_component.max_lod) continue; // Extract node location from Morton location code quadtree_type::node_type node_location = quadtree_type::location(node); quadtree_type::node_type node_location_x; quadtree_type::node_type node_location_y; geom::morton::decode(node_location, node_location_x, node_location_y); const double nodes_per_axis = std::exp2(node_depth); const double node_width = 2.0 / nodes_per_axis; // Determine node center on front face of unit BCBF cube. double3 center; center.y = -(nodes_per_axis * 0.5 * node_width) + node_width * 0.5; center.z = center.y; center.y += static_cast(node_location_x) * node_width; center.z += static_cast(node_location_y) * node_width; center.x = 1.0; // Rotate node center according to cube face /// @TODO Rather than rotating every center, "unrotate" observer position 6 times center = face_rotations[i] * center; // Project node center onto unit sphere double xx = center.x * center.x; double yy = center.y * center.y; double zz = center.z * center.z; center.x *= std::sqrt(std::max(0.0, 1.0 - yy * 0.5 - zz * 0.5 + yy * zz / 3.0)); center.y *= std::sqrt(std::max(0.0, 1.0 - xx * 0.5 - zz * 0.5 + xx * zz / 3.0)); center.z *= std::sqrt(std::max(0.0, 1.0 - xx * 0.5 - yy * 0.5 + xx * yy / 3.0)); // Scale node center by body radius center *= terrain_body.radius; center.y -= terrain_body.radius; //center *= 50.0; const double horizontal_fov = observer.camera->get_fov(); const double horizontal_resolution = 1920.0; const double distance = math::length(center - observer_cartesian); const double geometric_error = static_cast(524288.0 / std::exp2(node_depth)); const double screen_error = screen_space_error(horizontal_fov, horizontal_resolution, distance, geometric_error); if (screen_error > max_error) { //std::cout << screen_error << std::endl; quadtree.insert(quadtree_type::child(node, 0)); } } } }); }); // Handle meshes and models for each terrain patch registry.view().each( [&](entity::id terrain_eid, const auto& terrain_component, const auto& terrain_body) { // Retrieve terrain quadsphere terrain_quadsphere* quadsphere = terrain_quadspheres[terrain_eid]; // For each terrain quadsphere face for (int i = 0; i < 6; ++i) { terrain_quadsphere_face& quadsphere_face = quadsphere->faces[i]; const quadtree_type& quadtree = quadsphere_face.quadtree; // For each quadtree node for (auto node_it = quadtree.unordered_begin(); node_it != quadtree.unordered_end(); ++node_it) { quadtree_node_type node = *node_it; // Look up cached patch for this node auto patch_it = quadsphere_face.patches.find(node); // If there is no cached patch instance for this node if (patch_it == quadsphere_face.patches.end()) { // Construct a terrain patch terrain_patch* patch = new terrain_patch(); // Generate a patch mesh patch->mesh = generate_patch_mesh(i, *node_it, terrain_body.radius, terrain_component.elevation); //patch->mesh = generate_patch_mesh(i, *node_it, 50.0, terrain_component.elevation); // Generate a patch model patch->model = generate_patch_model(*patch->mesh, terrain_component.patch_material); // Construct patch model instance patch->model_instance = new scene::model_instance(patch->model); // Cache the terrain patch quadsphere_face.patches[node] = patch; // Add patch model instance to the patch scene collection if (patch_scene_collection) patch_scene_collection->add_object(patch->model_instance); } } // For each terrain pach for (auto patch_it = quadsphere_face.patches.begin(); patch_it != quadsphere_face.patches.end(); ++patch_it) { quadtree_node_type node = patch_it->first; // Set patch model instance active if its node is a leaf node, otherwise deactivate it bool active = (quadtree.contains(node) && quadtree.is_leaf(node)); patch_it->second->model_instance->set_active(active); } } }); } void terrain::set_patch_subdivisions(std::uint8_t n) { patch_subdivisions = n; // Rebuid patch base mesh { delete patch_base_mesh; patch_base_mesh = geom::meshes::grid_xy(2.0f, patch_subdivisions, patch_subdivisions); // Convert quads to triangle fans for (std::size_t i = 0; i < patch_base_mesh->get_faces().size(); ++i) { geom::mesh::face* face = patch_base_mesh->get_faces()[i]; std::size_t edge_count = 1; for (geom::mesh::edge* edge = face->edge->next; edge != face->edge; edge = edge->next) ++edge_count; if (edge_count > 3) { geom::poke_face(*patch_base_mesh, face->index); --i; } } } // Transform patch base mesh coordinates to match the front face of a BCBF cube const math::quaternion xy_to_zy = math::quaternion::rotate_y(-math::half_pi); for (geom::mesh::vertex* vertex: patch_base_mesh->get_vertices()) { vertex->position = xy_to_zy * vertex->position; vertex->position.x = 1.0f; } // Recalculate number of vertices per patch patch_vertex_count = patch_base_mesh->get_faces().size() * 3; // Resize patch vertex data buffer delete[] patch_vertex_data; patch_vertex_data = new float[patch_vertex_count * patch_vertex_size]; } void terrain::set_patch_scene_collection(scene::collection* collection) { patch_scene_collection = collection; } void terrain::set_max_error(double error) { max_error = error; } void terrain::on_terrain_construct(entity::registry& registry, entity::id entity_id, component::terrain& component) { terrain_quadsphere* quadsphere = new terrain_quadsphere(); terrain_quadspheres[entity_id] = quadsphere; } void terrain::on_terrain_destroy(entity::registry& registry, entity::id entity_id) { // Find terrain quadsphere for the given entity ID auto quadsphere_it = terrain_quadspheres.find(entity_id); if (quadsphere_it != terrain_quadspheres.end()) { terrain_quadsphere* quadsphere = quadsphere_it->second; // For each terrain quadsphere face for (int i = 0; i < 6; ++i) { terrain_quadsphere_face& quadsphere_face = quadsphere->faces[i]; for (auto patch_it = quadsphere_face.patches.begin(); patch_it != quadsphere_face.patches.end(); ++patch_it) { terrain_patch* patch = patch_it->second; if (patch_scene_collection) patch_scene_collection->remove_object(patch->model_instance); delete patch->model_instance; delete patch->model; delete patch->mesh; delete patch; } } // Free terrain quadsphere delete quadsphere; } // Remove terrain quadsphere from the map terrain_quadspheres.erase(quadsphere_it); } geom::mesh* terrain::generate_patch_mesh(std::uint8_t face_index, quadtree_node_type node, double body_radius, const std::function& elevation) const { // Extract node depth const quadtree_type::node_type depth = quadtree_type::depth(node); // Extract node Morton location code and decode location const quadtree_type::node_type location = quadtree_type::location(node); quadtree_type::node_type location_x; quadtree_type::node_type location_y; geom::morton::decode(location, location_x, location_y); const double nodes_per_axis = std::exp2(depth); const double scale_yz = 1.0 / nodes_per_axis; const double node_width = 2.0 / nodes_per_axis; // Determine vertex offset according to node location double offset_y = -(nodes_per_axis * 0.5 * node_width) + node_width * 0.5; double offset_z = offset_y; offset_y += static_cast(location_x) * node_width; offset_z += static_cast(location_y) * node_width; // Copy base mesh geom::mesh* patch_mesh = new geom::mesh(*patch_base_mesh); // Modify patch mesh vertex positions for (geom::mesh::vertex* v: patch_mesh->get_vertices()) { double3 position = math::type_cast(v->position); // Offset and scale vertex position position.y *= scale_yz; position.z *= scale_yz; position.y += offset_y; position.z += offset_z; // Rotate according to cube face position = face_rotations[face_index] * position; // Project onto unit sphere //position = math::normalize(position); // Cartesian Spherical Cube projection (KSC) /// @see https://catlikecoding.com/unity/tutorials/cube-sphere/ /// @see https://core.ac.uk/download/pdf/228552506.pdf double xx = position.x * position.x; double yy = position.y * position.y; double zz = position.z * position.z; position.x *= std::sqrt(std::max(0.0, 1.0 - yy * 0.5 - zz * 0.5 + yy * zz / 3.0)); position.y *= std::sqrt(std::max(0.0, 1.0 - xx * 0.5 - zz * 0.5 + xx * zz / 3.0)); position.z *= std::sqrt(std::max(0.0, 1.0 - xx * 0.5 - yy * 0.5 + xx * yy / 3.0)); // Calculate latitude and longitude of vertex position const double latitude = std::atan2(position.z, std::sqrt(position.x * position.x + position.y * position.y)); const double longitude = std::atan2(position.y, position.x); // Look up elevation at latitude and longitude and use to calculate radial distance const double radial_distance = body_radius + elevation(latitude, longitude); // Scale vertex position by radial distance position *= radial_distance; position.y -= body_radius; v->position = math::type_cast(position); } return patch_mesh; } model* terrain::generate_patch_model(const geom::mesh& patch_mesh, material* patch_material) const { // Barycentric coordinates static const float3 barycentric[3] = { {1, 0, 0}, {0, 1, 0}, {0, 0, 1} }; // Fill vertex data buffer float* v = patch_vertex_data; for (const geom::mesh::face* face: patch_mesh.get_faces()) { const geom::mesh::vertex* a = face->edge->vertex; const geom::mesh::vertex* b = face->edge->next->vertex; const geom::mesh::vertex* c = face->edge->previous->vertex; const geom::mesh::vertex* face_vertices[] = {a, b, c}; // Calculate facted normal float3 normal = math::normalize(math::cross(b->position - a->position, c->position - a->position)); for (std::size_t i = 0; i < 3; ++i) { const geom::mesh::vertex* vertex = face_vertices[i]; // Vertex position const float3& position = vertex->position; *(v++) = position.x; *(v++) = position.y; *(v++) = position.z; // Vertex UV coordinates (latitude, longitude) const float latitude = std::atan2(position.z, std::sqrt(position.x * position.x + position.y * position.y)); const float longitude = std::atan2(position.y, position.x); *(v++) = latitude; *(v++) = longitude; // Vertex normal *(v++) = normal.x; *(v++) = normal.y; *(v++) = normal.z; /// @TODO Vertex tangent *(v++) = 0.0f; *(v++) = 0.0f; *(v++) = 0.0f; *(v++) = 0.0f; // Vertex barycentric coordinates *(v++) = barycentric[i].x; *(v++) = barycentric[i].y; *(v++) = barycentric[i].z; // Vertex morph target (LOD transition) *(v++) = 0.0f; *(v++) = 0.0f; *(v++) = 0.0f; } } // Get triangle count of patch mesh std::size_t patch_triangle_count = patch_mesh.get_faces().size(); // Allocate patch model model* patch_model = new model(); // Resize model VBO and upload vertex data gl::vertex_buffer* vbo = patch_model->get_vertex_buffer(); vbo->resize(patch_triangle_count * 3 * patch_vertex_stride, patch_vertex_data); // Bind vertex attributes to model VAO gl::vertex_array* vao = patch_model->get_vertex_array(); std::size_t offset = 0; vao->bind_attribute(VERTEX_POSITION_LOCATION, *vbo, 3, gl::vertex_attribute_type::float_32, patch_vertex_stride, 0); offset += 3; vao->bind_attribute(VERTEX_TEXCOORD_LOCATION, *vbo, 2, gl::vertex_attribute_type::float_32, patch_vertex_stride, sizeof(float) * offset); offset += 2; vao->bind_attribute(VERTEX_NORMAL_LOCATION, *vbo, 3, gl::vertex_attribute_type::float_32, patch_vertex_stride, sizeof(float) * offset); offset += 3; vao->bind_attribute(VERTEX_TANGENT_LOCATION, *vbo, 4, gl::vertex_attribute_type::float_32, patch_vertex_stride, sizeof(float) * offset); offset += 4; vao->bind_attribute(VERTEX_BARYCENTRIC_LOCATION, *vbo, 3, gl::vertex_attribute_type::float_32, patch_vertex_stride, sizeof(float) * offset); offset += 3; vao->bind_attribute(VERTEX_TARGET_LOCATION, *vbo, 3, gl::vertex_attribute_type::float_32, patch_vertex_stride, sizeof(float) * offset); offset += 3; // Create model group model_group* patch_model_group = patch_model->add_group("terrain"); patch_model_group->set_material(patch_material); patch_model_group->set_drawing_mode(gl::drawing_mode::triangles); patch_model_group->set_start_index(0); patch_model_group->set_index_count(patch_triangle_count * 3); // Calculate model bounds geom::aabb bounds = geom::calculate_bounds(patch_mesh); patch_model->set_bounds(bounds); return patch_model; } double terrain::screen_space_error(double horizontal_fov, double horizontal_resolution, double distance, double geometric_error) { // Calculate view frustum width at given distance const double frustum_width = 2.0 * distance * std::tan(horizontal_fov * 0.5); return (geometric_error * horizontal_resolution) / frustum_width; } } // namespace system } // namespace entity