Files
watrbx-game-engine/App/v8world/SmoothClusterGeometry.cpp
2025-09-18 17:55:52 -04:00

720 lines
20 KiB
C++

#include "stdafx.h"
#include "v8world/SmoothClusterGeometry.h"
#include "v8world/MegaClusterPoly.h"
#include "v8datamodel/MegaCluster.h"
#include "v8world/KDTree.h"
#include "voxel2/MaterialTable.h"
#include "voxel2/Grid.h"
#include "voxel2/Mesher.h"
#include "voxel2/Conversion.h"
#include "BulletCollision/CollisionShapes/btBvhTriangleMeshShape.h"
#include "BulletCollision/CollisionShapes/btTriangleIndexVertexArray.h"
#include "BulletCollision/NarrowPhaseCollision/btRaycastCallback.h"
#include "BulletCollision/BroadPhaseCollision/btDbvt.h"
#include "rbx/Profiler.h"
FASTINTVARIABLE(SmoothTerrainPhysicsCacheSize, 16*1024*1024)
namespace RBX {
struct ChunkMeshShapeData
{
boost::scoped_array<Vector3> vertexPositions;
boost::scoped_array<unsigned char> vertexMaterials;
size_t vertexCount;
boost::scoped_array<unsigned int> solidIndices;
size_t solidTriangleCount;
boost::scoped_array<unsigned int> waterIndices;
size_t waterTriangleCount;
ChunkMeshShapeData(const Voxel2::Mesher::BasicMesh& geometry)
{
using namespace Voxel2::Mesher;
size_t triangleCount = geometry.indices.size() / 3;
size_t waterTriangles = 0;
std::vector<char> iswater(triangleCount);
for (size_t i = 0; i < triangleCount; ++i)
{
const Vertex& v0 = geometry.vertices[geometry.indices[3 * i + 0]];
const Vertex& v1 = geometry.vertices[geometry.indices[3 * i + 1]];
const Vertex& v2 = geometry.vertices[geometry.indices[3 * i + 2]];
iswater[i] = BasicMesh::isWater(v0, v1, v2);
waterTriangles += iswater[i];
}
vertexCount = geometry.vertices.size();
vertexPositions.reset(new Vector3[vertexCount]);
vertexMaterials.reset(new unsigned char[vertexCount]);
for (size_t i = 0; i < vertexCount; ++i)
{
const Vertex& v = geometry.vertices[i];
vertexPositions[i] = Voxel::cellSpaceToWorldSpace(v.position);
vertexMaterials[i] = v.material;
}
solidTriangleCount = triangleCount - waterTriangles;
solidIndices.reset(new unsigned int[solidTriangleCount * 3]);
waterTriangleCount = waterTriangles;
waterIndices.reset(new unsigned int[waterTriangleCount * 3]);
size_t solidOffset = 0;
size_t waterOffset = 0;
for (size_t i = 0; i < triangleCount; ++i)
{
unsigned int i0 = geometry.indices[3 * i + 0];
unsigned int i1 = geometry.indices[3 * i + 1];
unsigned int i2 = geometry.indices[3 * i + 2];
if (iswater[i])
{
waterIndices[waterOffset + 0] = i0;
waterIndices[waterOffset + 1] = i1;
waterIndices[waterOffset + 2] = i2;
waterOffset += 3;
}
else
{
solidIndices[solidOffset + 0] = i0;
solidIndices[solidOffset + 1] = i1;
solidIndices[solidOffset + 2] = i2;
solidOffset += 3;
}
}
}
};
class ChunkMeshShape: public btConcaveShape
{
public:
ChunkMeshShape(const Voxel2::Mesher::BasicMesh& geometry)
: data(geometry)
{
m_shapeType = TERRAIN_SHAPE_PROXYTYPE;
solidTree.build(data.vertexPositions.get(), data.vertexMaterials.get(), data.vertexCount, data.solidIndices.get(), data.solidTriangleCount);
waterTree.build(data.vertexPositions.get(), data.vertexMaterials.get(), data.vertexCount, data.waterIndices.get(), data.waterTriangleCount);
Vector3 aabbMin = solidTree.extentsMin.min(waterTree.extentsMin);
Vector3 aabbMax = solidTree.extentsMax.max(waterTree.extentsMax);
localAabbMin = btVector3(aabbMin.x, aabbMin.y, aabbMin.z);
localAabbMax = btVector3(aabbMax.x, aabbMax.y, aabbMax.z);
RBXPROFILER_COUNTER_ADD("memory/terrain/physics", getMemorySize());
}
~ChunkMeshShape()
{
RBXPROFILER_COUNTER_SUB("memory/terrain/physics", getMemorySize());
}
void castRay(KDTree::RayResult& result, const Vector3& raySource, const Vector3& rayTarget, bool ignoreWater)
{
solidTree.queryRay(result, raySource, rayTarget);
if (!ignoreWater)
waterTree.queryRay(result, raySource, rayTarget);
}
void processAllTriangles(btTriangleCallback* callback, const btVector3& aabbMin, const btVector3& aabbMax) const override
{
solidTree.queryAABB(callback, Vector3(aabbMin.x(), aabbMin.y(), aabbMin.z()), Vector3(aabbMax.x(), aabbMax.y(), aabbMax.z()));
}
void getAabb(const btTransform& t, btVector3& aabbMin, btVector3& aabbMax) const override
{
RBXCRASH();
}
PartMaterial getTriangleMaterial(unsigned int triangleIndex, const Vector3& localHitPoint)
{
return Voxel2::Conversion::getMaterialFromVoxelMaterial(solidTree.getMaterial(triangleIndex, localHitPoint));
}
void getBoundingSphere(btVector3& center, btScalar& radius) const override
{
center = (localAabbMin + localAabbMax) * 0.5f;
radius = (localAabbMax - localAabbMin).length() * 0.5f;
}
void setLocalScaling(const btVector3& scaling) override
{
RBXCRASH();
}
const btVector3& getLocalScaling() const override
{
static const btVector3 result(1, 1, 1);
return result;
}
void calculateLocalInertia(btScalar mass, btVector3& inertia) const override
{
inertia = btVector3(0, 0, 0);
}
virtual const char* getName() const override
{
return "SmoothClusterChunkMesh";
}
size_t getMemorySize() const
{
size_t result = 0;
result += data.vertexCount * (sizeof(data.vertexPositions[0]) + sizeof(data.vertexMaterials[0]));
result += data.solidTriangleCount * 3 * sizeof(data.solidIndices[0]);
result += data.waterTriangleCount * 3 * sizeof(data.waterIndices[0]);
result += solidTree.nodes.size() * sizeof(KDNode);
result += waterTree.nodes.size() * sizeof(KDNode);
return result;
}
const btVector3& getLocalAabbMin() const
{
return localAabbMin;
}
const btVector3& getLocalAabbMax() const
{
return localAabbMax;
}
private:
ChunkMeshShapeData data;
KDTree solidTree;
KDTree waterTree;
btVector3 localAabbMin;
btVector3 localAabbMax;
};
struct SmoothClusterGeometry::ChunkMesh
{
enum State
{
State_Dummy,
State_Ready,
};
Vector3int32 id;
State state;
btDbvt* tree;
btDbvtNode* treeNode;
shared_ptr<ChunkMeshShape> shape;
ChunkMesh(const Vector3int32& id)
: id(id)
, state(State_Dummy)
, tree(NULL)
, treeNode(NULL)
{
}
~ChunkMesh()
{
if (treeNode)
{
tree->remove(treeNode);
}
}
Voxel2::Region getRegion() const
{
return Voxel2::Region::fromChunk(id, TerrainPartitionSmooth::kChunkSizeLog2).expand(1);
}
void generateShape(MegaClusterInstance* mci)
{
RBXPROFILER_SCOPE("Physics", "generateShape");
RBXASSERT(state == State_Dummy);
state = State_Ready;
Voxel2::Grid* grid = mci->getSmoothGrid();
Voxel2::Region region = getRegion();
Voxel2::Box box = grid->read(region);
if (box.isEmpty())
return;
using namespace Voxel2::Mesher;
Options options = { mci->getMaterialTable(), /* generateWater= */ true };
BasicMesh geometry = generateGeometry(box, region.begin(), 0, options);
if (geometry.indices.empty())
return;
shape.reset(new ChunkMeshShape(geometry));
}
void updateTree()
{
RBXASSERT(state == State_Ready);
if (shape)
{
btDbvtVolume volume = btDbvtVolume::FromMM(shape->getLocalAabbMin(), shape->getLocalAabbMax());
tree->update(treeNode, volume);
}
else
{
tree->remove(treeNode);
treeNode = NULL;
}
}
};
SmoothClusterGeometry::SmoothClusterGeometry(Primitive* p)
: myPrim(p)
, grid(NULL)
, bulletChunksTree(NULL)
, gcChunkCountLast(0)
, gcUnusedMemory(0)
, gcUnusedMemoryNext(0)
{
grid = rbx_static_cast<MegaClusterInstance*>(p->getOwner())->getSmoothGrid();
partition.reset(new TerrainPartitionSmooth(grid));
bulletChunksTree = new (btAlignedAlloc(sizeof(btDbvt), 16)) btDbvt();
}
SmoothClusterGeometry::~SmoothClusterGeometry()
{
for (ChunkMap::iterator it = bulletChunks.begin(); it != bulletChunks.end(); ++it)
delete it->second;
if (bulletChunksTree)
{
bulletChunksTree->~btDbvt();
btAlignedFree(bulletChunksTree);
}
}
Geometry::GeometryType SmoothClusterGeometry::getGeometryType() const
{
return GEOMETRY_SMOOTHCLUSTER;
}
Geometry::CollideType SmoothClusterGeometry::getCollideType() const
{
return COLLIDE_BULLET;
}
float SmoothClusterGeometry::getRadius() const
{
return 0;
}
size_t SmoothClusterGeometry::closestSurfaceToPoint(const Vector3& pointInBody) const
{
return 0;
}
Plane SmoothClusterGeometry::getPlaneFromSurface(const size_t surfaceId) const
{
return Plane();
}
CoordinateFrame SmoothClusterGeometry::getSurfaceCoordInBody(const size_t surfaceId) const
{
return CoordinateFrame();
}
Vector3 SmoothClusterGeometry::getSurfaceNormalInBody(const size_t surfaceId) const
{
return Vector3();
}
size_t SmoothClusterGeometry::getMostAlignedSurface( const Vector3& vecInWorld, const G3D::Matrix3& objectR ) const
{
return 0;
}
int SmoothClusterGeometry::getNumSurfaces() const
{
return 0;
}
Vector3 SmoothClusterGeometry::getSurfaceVertInBody(const size_t surfaceId, const int vertId) const
{
return Vector3();
}
int SmoothClusterGeometry::getNumVertsInSurface(const size_t surfaceId) const
{
return 0;
}
bool SmoothClusterGeometry::vertOverlapsFace(const Vector3& pointInBody, const size_t surfaceId) const
{
return false;
}
bool SmoothClusterGeometry::findTouchingSurfacesConvex(const CoordinateFrame& myCf, size_t& myFaceId, const Geometry& otherGeom, const CoordinateFrame& otherCf, size_t& otherFaceId) const
{
return false;
}
bool SmoothClusterGeometry::FacesOverlapped(const CoordinateFrame& myCf, size_t& myFaceId, const Geometry& otherGeom, const CoordinateFrame& otherCf, size_t& otherFaceId, float tol) const
{
return false;
}
bool SmoothClusterGeometry::FaceVerticesOverlapped(const CoordinateFrame& myCf, size_t& myFaceId, const Geometry& otherGeom, const CoordinateFrame& otherCf, size_t& otherFaceId, float tol) const
{
return false;
}
bool SmoothClusterGeometry::FaceEdgesOverlapped(const CoordinateFrame& myCf, size_t& myFaceId, const Geometry& otherGeom, const CoordinateFrame& otherCf, size_t& otherFaceId, float tol) const
{
return false;
}
bool SmoothClusterGeometry::hitTest(const RbxRay& rayInMe, Vector3& localHitPoint, Vector3& surfaceNormal)
{
unsigned char material;
return castRay(rayInMe, localHitPoint, surfaceNormal, material, MC_SEARCH_RAY_MAX, /* ignoreWater= */ true);
}
bool SmoothClusterGeometry::hitTestTerrain(const RbxRay& rayInMe, Vector3& localHitPoint, int& surfId, CoordinateFrame& surfCf)
{
Vector3 normal;
unsigned char material;
if (castRay(rayInMe, localHitPoint, normal, material, MC_SEARCH_RAY_MAX, /* ignoreWater= */ true))
{
surfId = Math::getClosestObjectNormalId(normal, Matrix3::identity());
RBXASSERT(surfId != NORM_UNDEFINED);
surfCf.rotation = Math::getWellFormedRotForZVector(normal);
surfCf.translation = Math::toGrid(localHitPoint, Voxel::kCELL_SIZE);
// do not change CFrame origin in Y
surfCf.translation[surfId%3] = localHitPoint[surfId%3];
return true;
}
else
{
surfId = -1;
return false;
}
}
bool SmoothClusterGeometry::collidesWithGroundPlane(const CoordinateFrame& c, float yHeight) const
{
return false;
}
bool SmoothClusterGeometry::setUpBulletCollisionData()
{
return false;
}
struct DbvhRayTest
{
MegaClusterInstance* mci;
std::vector<SmoothClusterGeometry::ChunkMesh*> updatedChunks;
KDTree::RayResult result;
Vector3 rayFrom;
Vector3 rayTo;
bool ignoreWater;
struct DbvhNode
{
const btDbvtNode* node;
float tmin;
DbvhNode()
{
}
DbvhNode(const btDbvtNode* node, float tmin)
: node(node)
, tmin(tmin)
{
}
};
DbvhRayTest(MegaClusterInstance* mci, const Vector3& rayFrom, const Vector3& rayTo, bool ignoreWater)
: mci(mci)
, rayFrom(rayFrom)
, rayTo(rayTo)
, ignoreWater(ignoreWater)
{
}
void processLeaf(const btDbvtNode* leaf)
{
SmoothClusterGeometry::ChunkMesh* chunk = static_cast<SmoothClusterGeometry::ChunkMesh*>(leaf->data);
if (chunk->state == SmoothClusterGeometry::ChunkMesh::State_Dummy)
{
chunk->generateShape(mci);
updatedChunks.push_back(chunk);
}
if (chunk->shape)
{
chunk->shape->castRay(result, rayFrom, rayTo, ignoreWater);
}
}
void processTree(const btDbvtNode* root)
{
btVector3 rayOrigin = btVector3(rayFrom.x, rayFrom.y, rayFrom.z);
btVector3 rayDir = btVector3(rayTo.x, rayTo.y, rayTo.z) - rayOrigin;
// what about division by zero? --> just set rayDirection[i] to INF/BT_LARGE_FLOAT
btVector3 rayDirectionInverse;
rayDirectionInverse[0] = rayDir[0] == btScalar(0.0) ? btScalar(BT_LARGE_FLOAT) : btScalar(1.0) / rayDir[0];
rayDirectionInverse[1] = rayDir[1] == btScalar(0.0) ? btScalar(BT_LARGE_FLOAT) : btScalar(1.0) / rayDir[1];
rayDirectionInverse[2] = rayDir[2] == btScalar(0.0) ? btScalar(BT_LARGE_FLOAT) : btScalar(1.0) / rayDir[2];
unsigned int raySigns[3] = { rayDirectionInverse[0] < 0.0, rayDirectionInverse[1] < 0.0, rayDirectionInverse[2] < 0.0};
btAlignedObjectArray<DbvhNode> stack;
int stackOffset = 0;
stack.resize(btDbvt::DOUBLE_STACKSIZE);
stack[stackOffset++] = DbvhNode(root, 0);
btVector3 bounds[2];
while (stackOffset > 0)
{
DbvhNode node = stack[--stackOffset];
if (node.tmin >= result.fraction)
continue;
if (node.node->isinternal())
{
btScalar tmin[2] = {};
bool hit[2] = {};
bounds[0] = node.node->childs[0]->volume.Mins();
bounds[1] = node.node->childs[0]->volume.Maxs();
hit[0] = btRayAabb2(rayOrigin, rayDirectionInverse, raySigns, bounds, tmin[0], 0.f, 1.f);
bounds[0] = node.node->childs[1]->volume.Mins();
bounds[1] = node.node->childs[1]->volume.Maxs();
hit[1] = btRayAabb2(rayOrigin, rayDirectionInverse, raySigns, bounds, tmin[1], 0.f, 1.f);
if (stackOffset + 2 > stack.size())
stack.resize(stack.size() * 2);
// start with the node that's closer to the ray origin
int i0 = (tmin[0] < tmin[1]) ? 0 : 1;
int i1 = 1 - i0;
if (hit[i1])
stack[stackOffset++] = DbvhNode(node.node->childs[i1], tmin[i1]);
if (hit[i0])
stack[stackOffset++] = DbvhNode(node.node->childs[i0], tmin[i0]);
}
else
{
processLeaf(node.node);
}
}
}
};
bool SmoothClusterGeometry::castRay(const RbxRay& rayInMe, Vector3& localHitPoint, Vector3& surfaceNormal, unsigned char& surfaceMaterial, float maxDistance, bool ignoreWater)
{
Vector3 from = rayInMe.origin();
Vector3 to = rayInMe.origin() + maxDistance * rayInMe.direction();
MegaClusterInstance* mci = rbx_static_cast<MegaClusterInstance*>(myPrim->getOwner());
DbvhRayTest test(mci, from, to, ignoreWater);
if (bulletChunksTree->m_root)
test.processTree(bulletChunksTree->m_root);
for (size_t i = 0; i < test.updatedChunks.size(); ++i)
test.updatedChunks[i]->updateTree();
if (test.result.hasHit())
{
localHitPoint = from + test.result.fraction * (to - from);
surfaceNormal = test.result.tree->getTriangleNormal(test.result.triangle);
surfaceMaterial = test.result.tree->getMaterial(test.result.triangle, localHitPoint);
return true;
}
return false;
}
bool SmoothClusterGeometry::findCellsInBoundingBox(const Vector3& min, const Vector3& max)
{
Voxel2::Grid* grid = rbx_static_cast<MegaClusterInstance*>(myPrim->getOwner())->getSmoothGrid();
Voxel2::Region region = Voxel2::Region::fromExtents(min, max);
if (region.empty())
return false;
Voxel2::Box box = grid->read(region);
return !box.isEmpty();
}
void SmoothClusterGeometry::updateChunk(const Vector3int32& id)
{
partition->updateChunk(id);
ChunkMesh* chunk = new ChunkMesh(id);
Voxel2::Region region = chunk->getRegion();
btDbvtVolume bounds = btDbvtVolume::FromMM(btVector3(region.begin().x, region.begin().y, region.begin().z) * Voxel::kCELL_SIZE, btVector3(region.end().x, region.end().y, region.end().z) * Voxel::kCELL_SIZE);
chunk->tree = bulletChunksTree;
chunk->treeNode = bulletChunksTree->insert(bounds, chunk);
ChunkMesh*& slot = bulletChunks[id];
delete slot;
slot = chunk;
}
void SmoothClusterGeometry::updateAllChunks()
{
for (ChunkMap::iterator it = bulletChunks.begin(); it != bulletChunks.end(); ++it)
delete it->second;
bulletChunks.clear();
MegaClusterInstance* mci = rbx_static_cast<MegaClusterInstance*>(myPrim->getOwner());
Voxel2::Grid* grid = mci->getSmoothGrid();
std::vector<Voxel2::Region> regions = grid->getNonEmptyRegions();
for (size_t i = 0; i < regions.size(); ++i)
{
std::vector<Vector3int32> chunks = regions[i].expand(1).getChunkIds(TerrainPartitionSmooth::kChunkSizeLog2);
for (auto cid: chunks)
{
if (bulletChunks.find(cid) == bulletChunks.end())
updateChunk(cid);
}
}
}
void SmoothClusterGeometry::garbageCollectIncremental()
{
// To catch up with allocation rate we need to visit the number of allocated elements since last run plus a small constant
size_t allocatedCount = std::max(bulletChunks.size(), gcChunkCountLast) - gcChunkCountLast;
size_t visitCount = std::min(bulletChunks.size(), allocatedCount + 64);
ChunkMap::iterator it = bulletChunks.find(gcChunkKeyNext);
// This should only happen if last GC was ran with no items
if (it == bulletChunks.end())
it = bulletChunks.begin();
for (size_t j = 0; j < visitCount; ++j)
{
ChunkMesh* mesh = it->second;
if (mesh->shape && mesh->shape.unique())
{
size_t memorySize = mesh->shape->getMemorySize();
if (gcUnusedMemory > size_t(FInt::SmoothTerrainPhysicsCacheSize))
{
// Prevent overflow during subtracting since unused memory is an approximate measurement
gcUnusedMemory = std::max(gcUnusedMemory, memorySize) - memorySize;
RBXASSERT(mesh->state == ChunkMesh::State_Ready);
mesh->state = ChunkMesh::State_Dummy;
mesh->shape.reset();
}
else
{
gcUnusedMemoryNext += memorySize;
}
}
++it;
if (it == bulletChunks.end())
{
// We traversed all chunks, update our unused memory estimate
gcUnusedMemory = gcUnusedMemoryNext;
gcUnusedMemoryNext = 0;
it = bulletChunks.begin();
}
}
// If we stopped at the end, start at the beginning next time (INT_MAX is not a valid chunk id)
gcChunkCountLast = bulletChunks.size();
gcChunkKeyNext = (it == bulletChunks.end()) ? Vector3int32(INT_MAX, INT_MAX, INT_MAX) : it->first;
}
shared_ptr<btCollisionShape> SmoothClusterGeometry::getBulletChunkShape(const Vector3int32& id)
{
ChunkMap::iterator it = bulletChunks.find(id);
RBXASSERT(it != bulletChunks.end());
if (it == bulletChunks.end())
return shared_ptr<btCollisionShape>();
if (it->second->state == ChunkMesh::State_Dummy)
{
MegaClusterInstance* mci = rbx_static_cast<MegaClusterInstance*>(myPrim->getOwner());
it->second->generateShape(mci);
it->second->updateTree();
}
return it->second->shape;
}
PartMaterial SmoothClusterGeometry::getTriangleMaterial(btCollisionShape* collisionShape, unsigned int triangleIndex, const Vector3& localHitPoint)
{
ChunkMeshShape *chunkShape = boost::polymorphic_downcast<ChunkMeshShape*>(collisionShape);
return chunkShape->getTriangleMaterial(triangleIndex, localHitPoint);
}
} // namespace RBX