/* * Copyright (C) 2017 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 "navmesh.hpp" #include #include #include #include Navmesh::Navmesh() {} Navmesh::~Navmesh() { destroy(); } bool Navmesh::create(const std::vector& vertices, const std::vector& indices) { destroy(); if (indices.size() % 3 != 0) { std::cerr << "Navmesh::create(): index count is non multiple of 3\n"; return false; } // Copy vertices this->vertices.resize(vertices.size()); for (std::size_t i = 0; i < vertices.size(); ++i) { Navmesh::Vertex* vertex = new Navmesh::Vertex(); vertex->edge = nullptr; vertex->position = vertices[i]; vertex->flags = 0; vertex->index = i; this->vertices[i] = vertex; } // Allocate triangles triangles.resize(indices.size() / 3, nullptr); std::size_t currentTriangle = 0; // Load triangles std::map, Navmesh::Edge*> edgeMap; for (std::size_t i = 0; i < indices.size(); i += 3) { std::size_t a = indices[i]; std::size_t b = indices[i + 1]; std::size_t c = indices[i + 2]; if (a >= vertices.size() || b >= vertices.size() || c >= vertices.size()) { std::cerr << "Navmesh::create(): Mesh contains invalid index.\n"; destroy(); return false; } // Allocate three edges and a triangle Navmesh::Edge* ab = new Navmesh::Edge(); Navmesh::Edge* bc = new Navmesh::Edge(); Navmesh::Edge* ca = new Navmesh::Edge(); Navmesh::Triangle* triangle = new Navmesh::Triangle(); // Zero triangle flags triangle->flags = 0; // Zero edge flags ab->flags = 0; bc->flags = 0; ca->flags = 0; // Set triangle start edge triangle->edge = ab; // For each edge in this triangle std::size_t triangleIndices[] = {a, b, c}; Navmesh::Edge* triangleEdges[] = {ab, bc, ca}; for (std::size_t j = 0; j < 3; ++j) { // Set edge properties Navmesh::Edge* edge = triangleEdges[j]; edge->triangle = triangle; edge->vertex = this->vertices[triangleIndices[j]]; edge->previous = triangleEdges[(j + 2) % 3]; edge->next = triangleEdges[(j + 1) % 3]; edge->symmetric = nullptr; // Point vertex to this edge edge->vertex->edge = edge; // Check for symmetry std::pair symmetricPair(triangleIndices[(j + 1) % 3], triangleIndices[j]); std::map, Navmesh::Edge*>::iterator it = edgeMap.find(symmetricPair); if (it == edgeMap.end()) { // No symmetric edge found, insert this edge into the map std::pair pair(triangleIndices[j], triangleIndices[(j + 1) % 3]); edgeMap[pair] = edge; } else { // Symmetric edge found, connect edge->symmetric = it->second; it->second->symmetric = edge; } } // Set edge indices and add edges to the edge list ab->index = edges.size(); edges.push_back(ab); bc->index = edges.size(); edges.push_back(bc); ca->index = edges.size(); edges.push_back(ca); // Set triangle index and add triangle to the triangle list triangle->index = currentTriangle; triangles[currentTriangle++] = triangle; } calculateNormals(); return true; } void Navmesh::destroy() { for (std::size_t i = 0; i < vertices.size(); ++i) delete vertices[i]; for (std::size_t i = 0; i < edges.size(); ++i) delete edges[i]; for (std::size_t i = 0; i < triangles.size(); ++i) delete triangles[i]; vertices.clear(); edges.clear(); triangles.clear(); } bool Navmesh::loadOBJ(const std::string& filename) { // Open OBJ file std::ifstream file(filename.c_str()); if (!file.is_open()) { std::cerr << "Navmesh::loadOBJ(): Failed to open Wavefront OBJ file \"" << filename << "\"" << std::endl; return false; } // Read OBJ file from file stream if (!readOBJ(&file, filename)) { std::cerr << "Navmesh::loadOBJ(): Failed to read Wavefront OBJ file \"" << filename << "\"" << std::endl; file.close(); return false; } // Close OBJ file file.close(); return true; } void Navmesh::traverse(Navmesh::Triangle* startTriangle, const Vector3& startPosition, const Vector3& startVelocity, std::vector* traversal) { // Form initial traversal step Navmesh::Step step; step.triangle = startTriangle; step.start = normalize_barycentric(startPosition); step.end = step.start; step.edge = nullptr; // Determine the maximum distance of the traversal float maxDistance = glm::length(startVelocity); // Set initial velocity Vector3 velocity = startVelocity; // Traverse navmesh float distance = 0.0f; while (distance < maxDistance) { // Grab triangle coordinates const Vector3& a = step.triangle->edge->vertex->position; const Vector3& b = step.triangle->edge->next->vertex->position; const Vector3& c = step.triangle->edge->previous->vertex->position; // Calculate target position Vector3 cartesianStart = cartesian(step.start, a, b, c); Vector3 target = cartesianStart + velocity; // Find closest point on triangle to target position closestPointOnTriangle(target, step.triangle, &step.end, &step.edge); step.end = normalize_barycentric(step.end); // Add step to the traversal traversal->push_back(step); // Determine distance traveled by the step Vector3 cartesianEnd = cartesian(step.end, a, b, c); distance += glm::length(cartesianEnd - cartesianStart); // Check for no movement if (cartesianEnd == cartesianStart) { /* std::cout << "the same!\n"; if (step.edge == nullptr) std::cout << "\tand no edge\n"; else if (step.edge->symmetric == nullptr) { std::cout << "\tand disconnected\n"; //step.edge = step.edge->previous; } //break; */ } // Check if traversal is complete or edge is disconnected if (step.edge == nullptr || step.edge->symmetric == nullptr) { break; } // Recalculate velocity Quaternion rotation = glm::rotation(step.triangle->normal, step.edge->symmetric->triangle->normal); velocity = glm::normalize(rotation * velocity) * (maxDistance - distance); // Move to the next triangle step.triangle = step.edge->symmetric->triangle; // Calculate barycentric starting coordinates of the next step step.start = normalize_barycentric(barycentric(cartesianEnd, step.triangle->edge->vertex->position, step.triangle->edge->next->vertex->position, step.triangle->edge->previous->vertex->position)); step.end = step.start; step.edge = nullptr; } /* // Add triangle to visited list visited->push_back(triangle); // Grab triangle coordinates const glm::vec3& a = triangle->edge->vertex->position; const glm::vec3& b = triangle->edge->next->vertex->position; const glm::vec3& c = triangle->edge->previous->vertex->position; // Project target onto triangle glm::vec3 closestPoint; int edgeIndex = -1; WingedEdge::Edge* closestEdge = nullptr; project_on_triangle(target, a, b, c, &closestPoint, &edgeIndex); *end = closestPoint; // Determine if projected target is on an edge switch (edgeIndex) { case -1: // Projected target inside triangle return; case 0: closestEdge = triangle->edge; break; case 1: closestEdge = triangle->edge->next; break; case 2: closestEdge = triangle->edge->previous; break; } // If edge is not loose, repeat with connected triangle if (closestEdge->symmetric != nullptr) { for (std::size_t i = 0; i < visited->size() - 1; ++i) { if ((*visited)[i] == closestEdge->symmetric->triangle) return; } move(mesh, closestEdge->symmetric->triangle, closestPoint, target, visited, end); } */ } /* if (steerCCW.isTriggered()) { glm::quat rotation = glm::angleAxis(0.1f, navi.triangle->normal); navi_forward = glm::normalize(rotation * navi_forward); } if (steerCW.isTriggered()) { glm::quat rotation = glm::angleAxis(-0.1f, navi.triangle->normal); navi_forward = glm::normalize(rotation * navi_forward); } if (navigate.isTriggered()) { Mesh::Triangle* triangle = navi.triangle; glm::vec3 start = navi.position; glm::vec3 target = start + navi_forward * 0.02f; std::vector visited; glm::vec3 end; move(&sceneManager.getTerrain()->mesh, triangle, start, target, &visited, &end); Mesh::Triangle* end_triangle = visited[visited.size() - 1]; const glm::vec3& a = end_triangle->edge->vertex->position; const glm::vec3& b = end_triangle->edge->next->vertex->position; const glm::vec3& c = end_triangle->edge->previous->vertex->position; glm::vec3 p = (a + b + c) / 3.0f; const glm::vec3& n = end_triangle->normal; glm::vec3 projected_start = project_on_plane(start, p, n); // Calculate difference between positions glm::vec3 difference = end - projected_start; if (glm::dot(difference, difference) != 0.0f) { if (end_triangle != triangle) { glm::quat alignment = glm::rotation(triangle->normal, end_triangle->normal); navi_forward = glm::normalize(alignment * navi_forward); } } navi.position = end; navi.triangle = visited[visited.size() - 1]; } */ void Navmesh::calculateNormals() { for (std::size_t i = 0; i < triangles.size(); ++i) { Navmesh::Triangle* triangle = triangles[i]; // Calculate surface normal const Vector3& a = triangle->edge->vertex->position; const Vector3& b = triangle->edge->next->vertex->position; const Vector3& c = triangle->edge->previous->vertex->position; Vector3 ba = b - a; Vector3 ca = c - a; triangle->normal = glm::normalize(glm::cross(ba, ca)); } } bool Navmesh::readOBJ(std::istream* stream, const std::string& filename) { std::string line; std::vector vertices; std::vector indices; while (stream->good() && std::getline(*stream, line)) { // Tokenize line std::vector tokens; std::string token; std::istringstream linestream(line); while (linestream >> token) tokens.push_back(token); // Skip empty lines and comments if (tokens.empty() || tokens[0][0] == '#') continue; if (tokens[0] == "v") { if (tokens.size() != 4) { std::cerr << "Navmesh::readOBJ(): Invalid line \"" << line << "\" in file \"" << filename << "\"" << std::endl; return false; } Vector3 vertex; std::stringstream(tokens[1]) >> vertex.x; std::stringstream(tokens[2]) >> vertex.y; std::stringstream(tokens[3]) >> vertex.z; vertices.push_back(vertex); } else if (tokens[0] == "f") { if (tokens.size() != 4) { std::cerr << "Navmesh::readOBJ(): Invalid line \"" << line << "\" in file \"" << filename << "\"" << std::endl; return false; } std::size_t a, b, c; std::stringstream(tokens[1]) >> a; std::stringstream(tokens[2]) >> b; std::stringstream(tokens[3]) >> c; a -= 1; b -= 1; c -= 1; indices.push_back(a); indices.push_back(b); indices.push_back(c); } } return create(vertices, indices); } Vector3 Navmesh::barycentric(const Vector3& p, const Vector3& a, const Vector3& b, const Vector3& c) { Vector3 v0 = b - a; Vector3 v1 = c - a; Vector3 v2 = p - a; float d00 = glm::dot(v0, v0); float d01 = glm::dot(v0, v1); float d11 = glm::dot(v1, v1); float d20 = glm::dot(v2, v0); float d21 = glm::dot(v2, v1); float denom = d00 * d11 - d01 * d01; Vector3 result; result.y = (d11 * d20 - d01 * d21) / denom; // v result.z = (d00 * d21 - d01 * d20) / denom; // w result.x = 1.0f - result.y - result.z; // u return result; } Vector3 Navmesh::cartesian(const Vector3& p, const Vector3& a, const Vector3& b, const Vector3& c) { return a * p.x + b * p.y + c * p.z; } // code taken from Detour's dtClosestPtPointTriangle // @see https://github.com/recastnavigation/recastnavigation/blob/master/Detour/Source/DetourCommon.cpp // (zlib license) void Navmesh::closestPointOnTriangle(const Vector3& p, const Navmesh::Triangle* triangle, Vector3* closestPoint, Navmesh::Edge** closestEdge) { // Grab triangle coordinates const Vector3& a = triangle->edge->vertex->position; const Vector3& b = triangle->edge->next->vertex->position; const Vector3& c = triangle->edge->previous->vertex->position; // Check if P in vertex region outside A Vector3 ab = b - a; Vector3 ac = c - a; Vector3 ap = p - a; float d1 = glm::dot(ab, ap); float d2 = glm::dot(ac, ap); if (d1 <= 0.0f && d2 <= 0.0f) { // Barycentric coordinates (1, 0, 0) *closestPoint = Vector3(1.0f, 0.0f, 0.0f); *closestEdge = triangle->edge; return; } // Check if P in vertex region outside B Vector3 bp = p - b; float d3 = glm::dot(ab, bp); float d4 = glm::dot(ac, bp); if (d3 >= 0.0f && d4 <= d3) { // Barycentric coordinates (0, 1, 0) *closestPoint = Vector3(0.0f, 1.0f, 0.0f); *closestEdge = triangle->edge->next; return; } // Check if P in edge region of AB, if so return projection of P onto AB float vc = d1 * d4 - d3 * d2; if (vc <= 0.0f && d1 >= 0.0f && d3 <= 0.0f) { // barycentric coordinates (1-v,v,0) float v = d1 / (d1 - d3); *closestPoint = Vector3(1.0f - v, v, 0.0f); *closestEdge = triangle->edge; return; } // Check if P in vertex region outside C Vector3 cp = p - c; float d5 = glm::dot(ab, cp); float d6 = glm::dot(ac, cp); if (d6 >= 0.0f && d5 <= d6) { // Barycentric coordinates (0, 0, 1) *closestPoint = Vector3(0.0f, 0.0f, 1.0f); *closestEdge = triangle->edge->previous; return; } // Check if P in edge region of AC, if so return projection of P onto AC float vb = d5 * d2 - d1 * d6; if (vb <= 0.0f && d2 >= 0.0f && d6 <= 0.0f) { // Barycentric coordinates (1 - w, 0, w) float w = d2 / (d2 - d6); *closestPoint = Vector3(1.0f - w, 0.0f, w); *closestEdge = triangle->edge->previous; return; } // Check if P in edge region of BC, if so return projection of P onto BC float va = d3 * d6 - d5 * d4; if (va <= 0.0f && (d4 - d3) >= 0.0f && (d5 - d6) >= 0.0f) { // Barycentric coordinates (0, 1 - w, w) float w = (d4 - d3) / ((d4 - d3) + (d5 - d6)); *closestPoint = Vector3(0.0f, 1.0f - w, w); *closestEdge = triangle->edge->next; return; } // P inside face region. Compute Q through its barycentric coordinates (u, v, w) float denom = 1.0f / (va + vb + vc); float v = vb * denom; float w = vc * denom; *closestPoint = Vector3(1.0f - v - w, v, w); *closestEdge = nullptr; }