// David Eberly, Geometric Tools, Redmond WA 98052 // Copyright (c) 1998-2020 // Distributed under the Boost Software License, Version 1.0. // https://www.boost.org/LICENSE_1_0.txt // https://www.geometrictools.com/License/Boost/LICENSE_1_0.txt // Version: 4.0.2019.08.13 #pragma once #include #include #include #include #include #include // The Mesh class is designed to support triangulations of surfaces of a small // number of topologies. See the documents // https://www.geometrictools.com/MeshDifferentialGeometry.pdf // https://www.geometrictools.com/MeshFactory.pdf // for details. // // You must set the vertex attribute sources before calling Update(). // // The semantic "position" is required and its source must be an array // of Real with at least 3 channels so that positions are computed as // Vector3. // // The positions are assumed to be parameterized by texture coordinates // (u,v); the position is thought of as a function P(u,v). If texture // coordinates are provided, the semantic must be "tcoord". If texture // coordinates are not provided, default texture coordinates are computed // internally as described in the mesh factory document. // // The frame for the tangent space is optional. All vectors in the frame // must have sources that are arrays of Real with at least 3 channels per // attribute. If normal vectors are provided, the semantic must be // "normal". // // Two options are supported for tangent vectors. The first option is // that the tangents are surface derivatives dP/du and dP/dv, which are // not necessarily unit length or orthogonal. The semantics must be // "dpdu" and "dpdv". The second option is that the tangents are unit // length and orthogonal, with the infrequent possibility that a vertex // is degenerate in that dP/du and dP/dv are linearly dependent. The // semantics must be "tangent" and "bitangent". // // For each provided vertex attribute, a derived class can initialize // that attribute by overriding one of the Initialize*() functions whose // stubs are defined in this class. namespace WwiseGTE { enum class MeshTopology { ARBITRARY, RECTANGLE, CYLINDER, TORUS, DISK, SPHERE }; class MeshDescription { public: // Constructor for MeshTopology::ARBITRARY. The members topology, // numVertices, and numTriangles are set in the obvious manner. The // members numRows and numCols are set to zero. The remaining members // must be set explicitly by the client. MeshDescription(uint32_t inNumVertices, uint32_t inNumTriangles) : topology(MeshTopology::ARBITRARY), numVertices(inNumVertices), numTriangles(inNumTriangles), wantDynamicTangentSpaceUpdate(false), wantCCW(true), hasTangentSpaceVectors(false), allowUpdateFrame(false), numRows(0), numCols(0), rMax(0), cMax(0), rIncrement(0), constructed(false) { LogAssert(numVertices >= 3 && numTriangles >= 1, "Invalid input."); } // Constructor for topologies other than MeshTopology::ARBITRARY. // Compute the number of vertices and triangles for the mesh based on // the requested number of rows and columns. If the number of rows or // columns is invalid for the specified topology, they are modified to // be valid, in which case inNumRows/numRows and inNumCols/numCols can // differ. If the input topology is MeshTopology::ARBITRARY, then // inNumRows and inNumCols are assigned to numVertices and // numTriangles, respectively, and numRows and numCols are set to // zero. The remaining members must be set explicitly by the client. MeshDescription(MeshTopology inTopology, uint32_t inNumRows, uint32_t inNumCols) : topology(inTopology), wantDynamicTangentSpaceUpdate(false), wantCCW(true), hasTangentSpaceVectors(false), allowUpdateFrame(false), constructed(false) { switch (topology) { case MeshTopology::ARBITRARY: numVertices = inNumRows; numTriangles = inNumCols; numRows = 0; numCols = 0; rMax = 0; cMax = 0; rIncrement = 0; break; case MeshTopology::RECTANGLE: numRows = std::max(inNumRows, 2u); numCols = std::max(inNumCols, 2u); rMax = numRows - 1; cMax = numCols - 1; rIncrement = numCols; numVertices = (rMax + 1) * (cMax + 1); numTriangles = 2 * rMax * cMax; break; case MeshTopology::CYLINDER: numRows = std::max(inNumRows, 2u); numCols = std::max(inNumCols, 3u); rMax = numRows - 1; cMax = numCols; rIncrement = numCols + 1; numVertices = (rMax + 1) * (cMax + 1); numTriangles = 2 * rMax * cMax; break; case MeshTopology::TORUS: numRows = std::max(inNumRows, 2u); numCols = std::max(inNumCols, 3u); rMax = numRows; cMax = numCols; rIncrement = numCols + 1; numVertices = (rMax + 1) * (cMax + 1); numTriangles = 2 * rMax * cMax; break; case MeshTopology::DISK: numRows = std::max(inNumRows, 1u); numCols = std::max(inNumCols, 3u); rMax = numRows - 1; cMax = numCols; rIncrement = numCols + 1; numVertices = (rMax + 1) * (cMax + 1) + 1; numTriangles = 2 * rMax * cMax + numCols; break; case MeshTopology::SPHERE: numRows = std::max(inNumRows, 1u); numCols = std::max(inNumCols, 3u); rMax = numRows - 1; cMax = numCols; rIncrement = numCols + 1; numVertices = (rMax + 1) * (cMax + 1) + 2; numTriangles = 2 * rMax * cMax + 2 * numCols; break; } } MeshTopology topology; uint32_t numVertices; uint32_t numTriangles; std::vector vertexAttributes; IndexAttribute indexAttribute; bool wantDynamicTangentSpaceUpdate; // default: false bool wantCCW; // default: true // For internal use only. bool hasTangentSpaceVectors; bool allowUpdateFrame; uint32_t numRows, numCols; uint32_t rMax, cMax, rIncrement; // After an attempt to construct a Mesh or Mesh-derived object, // examine this value to determine whether the construction was // successful. bool constructed; }; template class Mesh { public: // Construction and destruction. This constructor is for ARBITRARY // topology. The vertices and indices must already be assigned by the // client. Derived classes use the protected constructor, but // assignment of vertices and indices occurs in the derived-class // constructors. Mesh(MeshDescription const& description, std::vector const& validTopologies) : mDescription(description), mPositions(nullptr), mNormals(nullptr), mTangents(nullptr), mBitangents(nullptr), mDPDUs(nullptr), mDPDVs(nullptr), mTCoords(nullptr), mPositionStride(0), mNormalStride(0), mTangentStride(0), mBitangentStride(0), mDPDUStride(0), mDPDVStride(0), mTCoordStride(0) { mDescription.constructed = false; for (auto const& topology : validTopologies) { if (mDescription.topology == topology) { mDescription.constructed = true; break; } } LogAssert(mDescription.indexAttribute.source != nullptr, "The mesh needs triangles/indices in Mesh constructor."); // Set sources for the requested vertex attributes. mDescription.hasTangentSpaceVectors = false; mDescription.allowUpdateFrame = mDescription.wantDynamicTangentSpaceUpdate; for (auto const& attribute : mDescription.vertexAttributes) { if (attribute.source != nullptr && attribute.stride > 0) { if (attribute.semantic == "position") { mPositions = reinterpret_cast*>(attribute.source); mPositionStride = attribute.stride; continue; } if (attribute.semantic == "normal") { mNormals = reinterpret_cast*>(attribute.source); mNormalStride = attribute.stride; continue; } if (attribute.semantic == "tangent") { mTangents = reinterpret_cast*>(attribute.source); mTangentStride = attribute.stride; mDescription.hasTangentSpaceVectors = true; continue; } if (attribute.semantic == "bitangent") { mBitangents = reinterpret_cast*>(attribute.source); mBitangentStride = attribute.stride; mDescription.hasTangentSpaceVectors = true; continue; } if (attribute.semantic == "dpdu") { mDPDUs = reinterpret_cast*>(attribute.source); mDPDUStride = attribute.stride; mDescription.hasTangentSpaceVectors = true; continue; } if (attribute.semantic == "dpdv") { mDPDVs = reinterpret_cast*>(attribute.source); mDPDVStride = attribute.stride; mDescription.hasTangentSpaceVectors = true; continue; } if (attribute.semantic == "tcoord") { mTCoords = reinterpret_cast*>(attribute.source); mTCoordStride = attribute.stride; continue; } } } LogAssert(mPositions != nullptr, "The mesh needs positions in Mesh constructor."); // The initial value of allowUpdateFrame is the client request // about wanting dynamic tangent-space updates. If the vertex // attributes do not include tangent-space vectors, then dynamic // updates are not necessary. If tangent-space vectors are // present, the update algorithm requires texture coordinates // (mTCoords must be nonnull) or must compute local coordinates // (mNormals must be nonnull). if (mDescription.allowUpdateFrame) { if (!mDescription.hasTangentSpaceVectors) { mDescription.allowUpdateFrame = false; } if (!mTCoords && !mNormals) { mDescription.allowUpdateFrame = false; } } if (mDescription.allowUpdateFrame) { mUTU.resize(mDescription.numVertices); mDTU.resize(mDescription.numVertices); } } virtual ~Mesh() { } // No copying or assignment is allowed. Mesh(Mesh const&) = delete; Mesh& operator=(Mesh const&) = delete; // Member accessors. inline MeshDescription const& GetDescription() const { return mDescription; } // If the underlying geometric data varies dynamically, call this // function to update whatever vertex attributes are specified by // the vertex pool. void Update() { LogAssert(mDescription.constructed, "The Mesh object failed the construction."); UpdatePositions(); if (mDescription.allowUpdateFrame) { UpdateFrame(); } else if (mNormals) { UpdateNormals(); } // else: The mesh has no frame data, so there is nothing to do. } protected: // Access the vertex attributes. inline Vector3& Position(uint32_t i) { char* positions = reinterpret_cast(mPositions); return *reinterpret_cast*>(positions + i * mPositionStride); } inline Vector3& Normal(uint32_t i) { char* normals = reinterpret_cast(mNormals); return *reinterpret_cast*>(normals + i * mNormalStride); } inline Vector3& Tangent(uint32_t i) { char* tangents = reinterpret_cast(mTangents); return *reinterpret_cast*>(tangents + i * mTangentStride); } inline Vector3& Bitangent(uint32_t i) { char* bitangents = reinterpret_cast(mBitangents); return *reinterpret_cast*>(bitangents + i * mBitangentStride); } inline Vector3& DPDU(uint32_t i) { char* dpdus = reinterpret_cast(mDPDUs); return *reinterpret_cast*>(dpdus + i * mDPDUStride); } inline Vector3& DPDV(uint32_t i) { char* dpdvs = reinterpret_cast(mDPDVs); return *reinterpret_cast*>(dpdvs + i * mDPDVStride); } inline Vector2& TCoord(uint32_t i) { char* tcoords = reinterpret_cast(mTCoords); return *reinterpret_cast*>(tcoords + i * mTCoordStride); } // Compute the indices for non-arbitrary topologies. This function is // called by derived classes. void ComputeIndices() { uint32_t t = 0; for (uint32_t r = 0, i = 0; r < mDescription.rMax; ++r) { uint32_t v0 = i, v1 = v0 + 1; i += mDescription.rIncrement; uint32_t v2 = i, v3 = v2 + 1; for (uint32_t c = 0; c < mDescription.cMax; ++c, ++v0, ++v1, ++v2, ++v3) { if (mDescription.wantCCW) { mDescription.indexAttribute.SetTriangle(t++, v0, v1, v2); mDescription.indexAttribute.SetTriangle(t++, v1, v3, v2); } else { mDescription.indexAttribute.SetTriangle(t++, v0, v2, v1); mDescription.indexAttribute.SetTriangle(t++, v1, v2, v3); } } } if (mDescription.topology == MeshTopology::DISK) { uint32_t v0 = 0, v1 = 1, v2 = mDescription.numVertices - 1; for (unsigned int c = 0; c < mDescription.numCols; ++c, ++v0, ++v1) { if (mDescription.wantCCW) { mDescription.indexAttribute.SetTriangle(t++, v0, v2, v1); } else { mDescription.indexAttribute.SetTriangle(t++, v0, v1, v2); } } } else if (mDescription.topology == MeshTopology::SPHERE) { uint32_t v0 = 0, v1 = 1, v2 = mDescription.numVertices - 2; for (uint32_t c = 0; c < mDescription.numCols; ++c, ++v0, ++v1) { if (mDescription.wantCCW) { mDescription.indexAttribute.SetTriangle(t++, v0, v2, v1); } else { mDescription.indexAttribute.SetTriangle(t++, v0, v1, v2); } } v0 = (mDescription.numRows - 1) * mDescription.numCols; v1 = v0 + 1; v2 = mDescription.numVertices - 1; for (uint32_t c = 0; c < mDescription.numCols; ++c, ++v0, ++v1) { if (mDescription.wantCCW) { mDescription.indexAttribute.SetTriangle(t++, v0, v2, v1); } else { mDescription.indexAttribute.SetTriangle(t++, v0, v1, v2); } } } } // The Update() function allows derived classes to use algorithms // different from least-squares fitting to compute the normals (when // no tangent-space information is requested) or to compute the frame // (normals and tangent space). The UpdatePositions() is a stub; the // base-class has no knowledge about how positions should be modified. // A derived class, however, might choose to use dynamic updating // and override UpdatePositions(). The base-class UpdateNormals() // computes vertex normals as averages of area-weighted triangle // normals (nonparametric approach). The base-class UpdateFrame() // uses a least-squares algorithm for estimating the tangent space // (parametric approach). virtual void UpdatePositions() { } virtual void UpdateNormals() { // Compute normal vector as normalized weighted averages of triangle // normal vectors. // Set the normals to zero to allow accumulation of triangle normals. Vector3 zero{ (Real)0, (Real)0, (Real)0 }; for (uint32_t i = 0; i < mDescription.numVertices; ++i) { Normal(i) = zero; } // Accumulate the triangle normals. for (uint32_t t = 0; t < mDescription.numTriangles; ++t) { // Get the positions for the triangle. uint32_t v0, v1, v2; mDescription.indexAttribute.GetTriangle(t, v0, v1, v2); Vector3 P0 = Position(v0); Vector3 P1 = Position(v1); Vector3 P2 = Position(v2); // Get the edge vectors. Vector3 E1 = P1 - P0; Vector3 E2 = P2 - P0; // Compute a triangle normal show length is twice the area of the // triangle. Vector3 triangleNormal = Cross(E1, E2); // Accumulate the triangle normals. Normal(v0) += triangleNormal; Normal(v1) += triangleNormal; Normal(v2) += triangleNormal; } // Normalize the normals. for (uint32_t i = 0; i < mDescription.numVertices; ++i) { Normalize(Normal(i), true); } } virtual void UpdateFrame() { if (!mTCoords) { // We need to compute vertex normals first in order to compute // local texture coordinates. The vertex normals are recomputed // later based on estimated tangent vectors. UpdateNormals(); } // Use the least-squares algorithm to estimate the tangent-space vectors // and, if requested, normal vectors. Matrix<2, 2, Real> zero2x2; // initialized to zero Matrix<3, 2, Real> zero3x2; // initialized to zero std::fill(mUTU.begin(), mUTU.end(), zero2x2); std::fill(mDTU.begin(), mDTU.end(), zero3x2); for (uint32_t t = 0; t < mDescription.numTriangles; ++t) { // Get the positions and differences for the triangle. uint32_t v0, v1, v2; mDescription.indexAttribute.GetTriangle(t, v0, v1, v2); Vector3 P0 = Position(v0); Vector3 P1 = Position(v1); Vector3 P2 = Position(v2); Vector3 D10 = P1 - P0; Vector3 D20 = P2 - P0; Vector3 D21 = P2 - P1; if (mTCoords) { // Get the texture coordinates and differences for the triangle. Vector2 C0 = TCoord(v0); Vector2 C1 = TCoord(v1); Vector2 C2 = TCoord(v2); Vector2 U10 = C1 - C0; Vector2 U20 = C2 - C0; Vector2 U21 = C2 - C1; // Compute the outer products. Matrix<2, 2, Real> outerU10 = OuterProduct(U10, U10); Matrix<2, 2, Real> outerU20 = OuterProduct(U20, U20); Matrix<2, 2, Real> outerU21 = OuterProduct(U21, U21); Matrix<3, 2, Real> outerD10 = OuterProduct(D10, U10); Matrix<3, 2, Real> outerD20 = OuterProduct(D20, U20); Matrix<3, 2, Real> outerD21 = OuterProduct(D21, U21); // Keep a running sum of U^T*U and D^T*U. mUTU[v0] += outerU10 + outerU20; mUTU[v1] += outerU10 + outerU21; mUTU[v2] += outerU20 + outerU21; mDTU[v0] += outerD10 + outerD20; mDTU[v1] += outerD10 + outerD21; mDTU[v2] += outerD20 + outerD21; } else { // Compute local coordinates and differences for the triangle. Vector3 basis[3]; basis[0] = Normal(v0); ComputeOrthogonalComplement(1, basis, true); Vector2 U10{ Dot(basis[1], D10), Dot(basis[2], D10) }; Vector2 U20{ Dot(basis[1], D20), Dot(basis[2], D20) }; mUTU[v0] += OuterProduct(U10, U10) + OuterProduct(U20, U20); mDTU[v0] += OuterProduct(D10, U10) + OuterProduct(D20, U20); basis[0] = Normal(v1); ComputeOrthogonalComplement(1, basis, true); Vector2 U01{ Dot(basis[1], D10), Dot(basis[2], D10) }; Vector2 U21{ Dot(basis[1], D21), Dot(basis[2], D21) }; mUTU[v1] += OuterProduct(U01, U01) + OuterProduct(U21, U21); mDTU[v1] += OuterProduct(D10, U01) + OuterProduct(D21, U21); basis[0] = Normal(v2); ComputeOrthogonalComplement(1, basis, true); Vector2 U02{ Dot(basis[1], D20), Dot(basis[2], D20) }; Vector2 U12{ Dot(basis[1], D21), Dot(basis[2], D21) }; mUTU[v2] += OuterProduct(U02, U02) + OuterProduct(U12, U12); mDTU[v2] += OuterProduct(D20, U02) + OuterProduct(D21, U12); } } for (uint32_t i = 0; i < mDescription.numVertices; ++i) { Matrix<3, 2, Real> jacobian = mDTU[i] * Inverse(mUTU[i]); Vector3 basis[3]; basis[0] = { jacobian(0, 0), jacobian(1, 0), jacobian(2, 0) }; basis[1] = { jacobian(0, 1), jacobian(1, 1), jacobian(2, 1) }; if (mDPDUs) { DPDU(i) = basis[0]; } if (mDPDVs) { DPDV(i) = basis[1]; } ComputeOrthogonalComplement(2, basis, true); if (mNormals) { Normal(i) = basis[2]; } if (mTangents) { Tangent(i) = basis[0]; } if (mBitangents) { Bitangent(i) = basis[1]; } } } // Constructor inputs. // The client requests this via the constructor; however, if it is // requested and the vertex attributes do not contain entries for // "tangent", "bitangent", "dpdu", or "dpdv", then this member is // set to false. MeshDescription mDescription; // Copied from mVertexAttributes when available. Vector3* mPositions; Vector3* mNormals; Vector3* mTangents; Vector3* mBitangents; Vector3* mDPDUs; Vector3* mDPDVs; Vector2* mTCoords; size_t mPositionStride; size_t mNormalStride; size_t mTangentStride; size_t mBitangentStride; size_t mDPDUStride; size_t mDPDVStride; size_t mTCoordStride; // When dynamic tangent-space updates are requested, the update algorithm // requires texture coordinates (user-specified or non-local). It is // possible to create a vertex-adjacent set (with indices into the // vertex array) for each mesh vertex; however, instead we rely on a // triangle iteration and incrementally store the information needed for // the estimation of the tangent space. Each vertex has associated // matrices D and U, but we need to store only U^T*U and D^T*U. See the // PDF for details. std::vector> mUTU; std::vector> mDTU; }; }