add raycast accelerator for btHeightfieldTerrainShape, thanks to Marc Zylann, see https://github.com/bulletphysics/bullet3/pull/2062
it can be disabled by setting the flag cb.m_flags |= btTriangleRaycastCallback::kF_DisableHeightfieldAccelerator; acceleration is disabled for z axis up. add btHeightfieldTerrainShape example to example browser
This commit is contained in:
@@ -313,35 +313,6 @@ public:
|
|||||||
m_guiHelper->getRenderInterface()->drawLines(&points[0].m_floats[0], lineColor, points.size(), sizeof(btVector3FloatData), &indices[0], indices.size(), 1);
|
m_guiHelper->getRenderInterface()->drawLines(&points[0].m_floats[0], lineColor, points.size(), sizeof(btVector3FloatData), &indices[0], indices.size(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if 0
|
|
||||||
glDisable (GL_LIGHTING);
|
|
||||||
glColor3f (0.0, 1.0, 0.0);
|
|
||||||
glBegin (GL_LINES);
|
|
||||||
int i;
|
|
||||||
|
|
||||||
for (i = 0; i < NUMRAYS; i++)
|
|
||||||
{
|
|
||||||
glVertex3f (source[i][0], source[i][1], source[i][2]);
|
|
||||||
glVertex3f (hit[i][0], hit[i][1], hit[i][2]);
|
|
||||||
}
|
|
||||||
glEnd ();
|
|
||||||
glColor3f (1.0, 1.0, 1.0);
|
|
||||||
glBegin (GL_LINES);
|
|
||||||
for (i = 0; i < NUMRAYS; i++)
|
|
||||||
{
|
|
||||||
glVertex3f (hit[i][0], hit[i][1], hit[i][2]);
|
|
||||||
glVertex3f (hit[i][0] + normal[i][0], hit[i][1] + normal[i][1], hit[i][2] + normal[i][2]);
|
|
||||||
}
|
|
||||||
glEnd ();
|
|
||||||
glColor3f (0.0, 1.0, 1.0);
|
|
||||||
glBegin (GL_POINTS);
|
|
||||||
for ( i = 0; i < NUMRAYS; i++)
|
|
||||||
{
|
|
||||||
glVertex3f (hit[i][0], hit[i][1], hit[i][2]);
|
|
||||||
}
|
|
||||||
glEnd ();
|
|
||||||
glEnable (GL_LIGHTING);
|
|
||||||
#endif //USE_GRAPHICAL_BENCHMARK
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -438,6 +438,8 @@ struct CommonRigidBodyBase : public CommonExampleInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
virtual void renderScene()
|
virtual void renderScene()
|
||||||
|
{
|
||||||
|
if (m_dynamicsWorld)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
m_guiHelper->syncPhysicsToGraphics(m_dynamicsWorld);
|
m_guiHelper->syncPhysicsToGraphics(m_dynamicsWorld);
|
||||||
@@ -447,6 +449,7 @@ struct CommonRigidBodyBase : public CommonExampleInterface
|
|||||||
m_guiHelper->render(m_dynamicsWorld);
|
m_guiHelper->render(m_dynamicsWorld);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif //COMMON_RIGID_BODY_SETUP_H
|
#endif //COMMON_RIGID_BODY_SETUP_H
|
||||||
|
|||||||
@@ -207,6 +207,8 @@ SET(BulletExampleBrowser_SRCS
|
|||||||
../MultiThreadedDemo/MultiThreadedDemo.h
|
../MultiThreadedDemo/MultiThreadedDemo.h
|
||||||
../MultiThreadedDemo/CommonRigidBodyMTBase.cpp
|
../MultiThreadedDemo/CommonRigidBodyMTBase.cpp
|
||||||
../MultiThreadedDemo/CommonRigidBodyMTBase.h
|
../MultiThreadedDemo/CommonRigidBodyMTBase.h
|
||||||
|
../Heightfield/HeightfieldExample.cpp
|
||||||
|
../Heightfield/HeightfieldExample.h
|
||||||
../BlockSolver/btBlockSolver.cpp
|
../BlockSolver/btBlockSolver.cpp
|
||||||
../BlockSolver/btBlockSolver.h
|
../BlockSolver/btBlockSolver.h
|
||||||
../BlockSolver/BlockSolverExample.cpp
|
../BlockSolver/BlockSolverExample.cpp
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "../BlockSolver/RigidBodyBoxes.h"
|
#include "../BlockSolver/RigidBodyBoxes.h"
|
||||||
#include "LinearMath/btAlignedObjectArray.h"
|
#include "LinearMath/btAlignedObjectArray.h"
|
||||||
#include "EmptyExample.h"
|
#include "EmptyExample.h"
|
||||||
|
#include "../Heightfield/HeightfieldExample.h"
|
||||||
#include "../RenderingExamples/RenderInstancingDemo.h"
|
#include "../RenderingExamples/RenderInstancingDemo.h"
|
||||||
#include "../RenderingExamples/CoordinateSystemDemo.h"
|
#include "../RenderingExamples/CoordinateSystemDemo.h"
|
||||||
#include "../RenderingExamples/RaytracerSetup.h"
|
#include "../RenderingExamples/RaytracerSetup.h"
|
||||||
@@ -157,8 +158,8 @@ static ExampleEntry gDefaultExamples[] =
|
|||||||
ExampleEntry(1, "Stack MultiBody MLCP PGS", "Create a stack of blocks, with heavy block at the top", BlockSolverExampleCreateFunc, BLOCK_SOLVER_SCENE_MB_STACK + BLOCK_SOLVER_MLCP_PGS),
|
ExampleEntry(1, "Stack MultiBody MLCP PGS", "Create a stack of blocks, with heavy block at the top", BlockSolverExampleCreateFunc, BLOCK_SOLVER_SCENE_MB_STACK + BLOCK_SOLVER_MLCP_PGS),
|
||||||
ExampleEntry(1, "Stack MultiBody MLCP Dantzig", "Create a stack of blocks, with heavy block at the top", BlockSolverExampleCreateFunc, BLOCK_SOLVER_SCENE_MB_STACK + BLOCK_SOLVER_MLCP_DANTZIG),
|
ExampleEntry(1, "Stack MultiBody MLCP Dantzig", "Create a stack of blocks, with heavy block at the top", BlockSolverExampleCreateFunc, BLOCK_SOLVER_SCENE_MB_STACK + BLOCK_SOLVER_MLCP_DANTZIG),
|
||||||
ExampleEntry(1, "Stack MultiBody Block", "Create a stack of blocks, with heavy block at the top", BlockSolverExampleCreateFunc, BLOCK_SOLVER_SCENE_MB_STACK + BLOCK_SOLVER_BLOCK),
|
ExampleEntry(1, "Stack MultiBody Block", "Create a stack of blocks, with heavy block at the top", BlockSolverExampleCreateFunc, BLOCK_SOLVER_SCENE_MB_STACK + BLOCK_SOLVER_BLOCK),
|
||||||
ExampleEntry(1, "Stack RigidBody SI", "Create a stack of blocks, with heavy block at the top", RigidBodyBoxesCreateFunc, BLOCK_SOLVER_SI),
|
//ExampleEntry(1, "Stack RigidBody SI", "Create a stack of blocks, with heavy block at the top", RigidBodyBoxesCreateFunc, BLOCK_SOLVER_SI),
|
||||||
ExampleEntry(1, "Stack RigidBody Block", "Create a stack of blocks, with heavy block at the top", RigidBodyBoxesCreateFunc, BLOCK_SOLVER_BLOCK),
|
//ExampleEntry(1, "Stack RigidBody Block", "Create a stack of blocks, with heavy block at the top", RigidBodyBoxesCreateFunc, BLOCK_SOLVER_BLOCK),
|
||||||
|
|
||||||
ExampleEntry(0, "Inverse Dynamics"),
|
ExampleEntry(0, "Inverse Dynamics"),
|
||||||
ExampleEntry(1, "Inverse Dynamics URDF", "Create a btMultiBody from URDF. Create an inverse MultiBodyTree model from that. Use either decoupled PD control or computed torque control using the inverse model to track joint position targets", InverseDynamicsExampleCreateFunc, BT_ID_LOAD_URDF),
|
ExampleEntry(1, "Inverse Dynamics URDF", "Create a btMultiBody from URDF. Create an inverse MultiBodyTree model from that. Use either decoupled PD control or computed torque control using the inverse model to track joint position targets", InverseDynamicsExampleCreateFunc, BT_ID_LOAD_URDF),
|
||||||
@@ -234,6 +235,7 @@ static ExampleEntry gDefaultExamples[] =
|
|||||||
ExampleEntry(1, "Convex vs Mesh", "Benchmark the performance and stability of rigid bodies using convex hull collision shapes (btConvexHullShape), resting on a triangle mesh, btBvhTriangleMeshShape.", BenchmarkCreateFunc, 6),
|
ExampleEntry(1, "Convex vs Mesh", "Benchmark the performance and stability of rigid bodies using convex hull collision shapes (btConvexHullShape), resting on a triangle mesh, btBvhTriangleMeshShape.", BenchmarkCreateFunc, 6),
|
||||||
ExampleEntry(1, "Raycast", "Benchmark the performance of the btCollisionWorld::rayTest. Note that currently the rays are not rendered.", BenchmarkCreateFunc, 7),
|
ExampleEntry(1, "Raycast", "Benchmark the performance of the btCollisionWorld::rayTest. Note that currently the rays are not rendered.", BenchmarkCreateFunc, 7),
|
||||||
ExampleEntry(1, "Convex Pack", "Benchmark the performance of the convex hull primitive.", BenchmarkCreateFunc, 8),
|
ExampleEntry(1, "Convex Pack", "Benchmark the performance of the convex hull primitive.", BenchmarkCreateFunc, 8),
|
||||||
|
ExampleEntry(1, "Heightfield", "Raycast against a btHeightfieldTerrainShape", HeightfieldExampleCreateFunc),
|
||||||
//#endif
|
//#endif
|
||||||
|
|
||||||
ExampleEntry(0, "Importers"),
|
ExampleEntry(0, "Importers"),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "OpenGLGuiHelper.h"
|
#include "OpenGLGuiHelper.h"
|
||||||
|
|
||||||
#include "btBulletDynamicsCommon.h"
|
#include "btBulletDynamicsCommon.h"
|
||||||
|
#include "BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h"
|
||||||
#include "../CommonInterfaces/CommonGraphicsAppInterface.h"
|
#include "../CommonInterfaces/CommonGraphicsAppInterface.h"
|
||||||
#include "../CommonInterfaces/CommonRenderInterface.h"
|
#include "../CommonInterfaces/CommonRenderInterface.h"
|
||||||
#include "Bullet3Common/b3Scalar.h"
|
#include "Bullet3Common/b3Scalar.h"
|
||||||
@@ -255,6 +255,38 @@ void OpenGLGuiHelper::createRigidBodyGraphicsObject(btRigidBody* body, const btV
|
|||||||
createCollisionObjectGraphicsObject(body, color);
|
createCollisionObjectGraphicsObject(body, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MyTriangleCollector2 : public btTriangleCallback
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
btAlignedObjectArray<GLInstanceVertex>* m_pVerticesOut;
|
||||||
|
btAlignedObjectArray<int>* m_pIndicesOut;
|
||||||
|
|
||||||
|
MyTriangleCollector2()
|
||||||
|
{
|
||||||
|
m_pVerticesOut = 0;
|
||||||
|
m_pIndicesOut = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void processTriangle(btVector3* tris, int partId, int triangleIndex)
|
||||||
|
{
|
||||||
|
for (int k = 0; k < 3; k++)
|
||||||
|
{
|
||||||
|
GLInstanceVertex v;
|
||||||
|
v.xyzw[3] = 0;
|
||||||
|
v.uv[0] = v.uv[1] = 0.5f;
|
||||||
|
btVector3 normal = (tris[0] - tris[1]).cross(tris[0] - tris[2]);
|
||||||
|
normal.safeNormalize();
|
||||||
|
for (int l = 0; l < 3; l++)
|
||||||
|
{
|
||||||
|
v.xyzw[l] = tris[k][l];
|
||||||
|
v.normal[l] = normal[l];
|
||||||
|
}
|
||||||
|
m_pIndicesOut->push_back(m_pVerticesOut->size());
|
||||||
|
m_pVerticesOut->push_back(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
void OpenGLGuiHelper::createCollisionObjectGraphicsObject(btCollisionObject* body, const btVector3& color)
|
void OpenGLGuiHelper::createCollisionObjectGraphicsObject(btCollisionObject* body, const btVector3& color)
|
||||||
{
|
{
|
||||||
if (body->getUserIndex() < 0)
|
if (body->getUserIndex() < 0)
|
||||||
@@ -409,6 +441,30 @@ void OpenGLGuiHelper::createCollisionShapeGraphicsObject(btCollisionShape* colli
|
|||||||
//if (collisionShape->getShapeType()==BOX_SHAPE_PROXYTYPE)
|
//if (collisionShape->getShapeType()==BOX_SHAPE_PROXYTYPE)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (collisionShape->getShapeType() == TERRAIN_SHAPE_PROXYTYPE)
|
||||||
|
{
|
||||||
|
const btHeightfieldTerrainShape* heightField = static_cast<const btHeightfieldTerrainShape*>(collisionShape);
|
||||||
|
MyTriangleCollector2 col;
|
||||||
|
col.m_pVerticesOut = &gfxVertices;
|
||||||
|
col.m_pIndicesOut = &indices;
|
||||||
|
btVector3 aabbMin, aabbMax;
|
||||||
|
for (int k = 0; k < 3; k++)
|
||||||
|
{
|
||||||
|
aabbMin[k] = -BT_LARGE_FLOAT;
|
||||||
|
aabbMax[k] = BT_LARGE_FLOAT;
|
||||||
|
}
|
||||||
|
heightField->processAllTriangles(&col, aabbMin, aabbMax);
|
||||||
|
if (gfxVertices.size() && indices.size())
|
||||||
|
{
|
||||||
|
int shapeId = m_data->m_glApp->m_renderer->registerShape(&gfxVertices[0].xyzw[0], gfxVertices.size(), &indices[0], indices.size());
|
||||||
|
collisionShape->setUserIndex(shapeId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (collisionShape->getShapeType() == SOFTBODY_SHAPE_PROXYTYPE)
|
if (collisionShape->getShapeType() == SOFTBODY_SHAPE_PROXYTYPE)
|
||||||
{
|
{
|
||||||
computeSoftBodyVertices(collisionShape, gfxVertices, indices);
|
computeSoftBodyVertices(collisionShape, gfxVertices, indices);
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ project "App_BulletExampleBrowser"
|
|||||||
"../Collision/Internal/*",
|
"../Collision/Internal/*",
|
||||||
"../Benchmarks/*",
|
"../Benchmarks/*",
|
||||||
"../MultiThreadedDemo/*",
|
"../MultiThreadedDemo/*",
|
||||||
|
"../Heightfield/HeightfieldExample.*",
|
||||||
"../CommonInterfaces/*.h",
|
"../CommonInterfaces/*.h",
|
||||||
"../ForkLift/ForkLiftDemo.*",
|
"../ForkLift/ForkLiftDemo.*",
|
||||||
"../Importers/**",
|
"../Importers/**",
|
||||||
|
|||||||
1116
examples/Heightfield/HeightfieldExample.cpp
Normal file
1116
examples/Heightfield/HeightfieldExample.cpp
Normal file
File diff suppressed because it is too large
Load Diff
21
examples/Heightfield/HeightfieldExample.h
Normal file
21
examples/Heightfield/HeightfieldExample.h
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
Bullet Continuous Collision Detection and Physics Library
|
||||||
|
Copyright (c) 2015 Google Inc. http://bulletphysics.org
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied warranty.
|
||||||
|
In no event will the authors be held liable for any damages arising from the use of this software.
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it freely,
|
||||||
|
subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
|
||||||
|
3. This notice may not be removed or altered from any source distribution.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef HEIGHTFIELD_EXAMPLE_H
|
||||||
|
#define HEIGHTFIELD_EXAMPLE_H
|
||||||
|
|
||||||
|
class CommonExampleInterface* HeightfieldExampleCreateFunc(struct CommonExampleOptions& options);
|
||||||
|
|
||||||
|
#endif //HEIGHTFIELD_EXAMPLE_H
|
||||||
@@ -22,6 +22,7 @@ subject to the following restrictions:
|
|||||||
#include "BulletCollision/CollisionShapes/btSphereShape.h" //for raycasting
|
#include "BulletCollision/CollisionShapes/btSphereShape.h" //for raycasting
|
||||||
#include "BulletCollision/CollisionShapes/btBvhTriangleMeshShape.h" //for raycasting
|
#include "BulletCollision/CollisionShapes/btBvhTriangleMeshShape.h" //for raycasting
|
||||||
#include "BulletCollision/CollisionShapes/btScaledBvhTriangleMeshShape.h" //for raycasting
|
#include "BulletCollision/CollisionShapes/btScaledBvhTriangleMeshShape.h" //for raycasting
|
||||||
|
#include "BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h" //for raycasting
|
||||||
#include "BulletCollision/NarrowPhaseCollision/btRaycastCallback.h"
|
#include "BulletCollision/NarrowPhaseCollision/btRaycastCallback.h"
|
||||||
#include "BulletCollision/CollisionShapes/btCompoundShape.h"
|
#include "BulletCollision/CollisionShapes/btCompoundShape.h"
|
||||||
#include "BulletCollision/NarrowPhaseCollision/btSubSimplexConvexCast.h"
|
#include "BulletCollision/NarrowPhaseCollision/btSubSimplexConvexCast.h"
|
||||||
@@ -413,6 +414,21 @@ void btCollisionWorld::rayTestSingleInternal(const btTransform& rayFromTrans, co
|
|||||||
rcb.m_hitFraction = resultCallback.m_closestHitFraction;
|
rcb.m_hitFraction = resultCallback.m_closestHitFraction;
|
||||||
triangleMesh->performRaycast(&rcb, rayFromLocalScaled, rayToLocalScaled);
|
triangleMesh->performRaycast(&rcb, rayFromLocalScaled, rayToLocalScaled);
|
||||||
}
|
}
|
||||||
|
else if (((resultCallback.m_flags&btTriangleRaycastCallback::kF_DisableHeightfieldAccelerator)==0)
|
||||||
|
&& collisionShape->getShapeType() == TERRAIN_SHAPE_PROXYTYPE &&
|
||||||
|
(((btHeightfieldTerrainShape*)collisionShape)->getUpAxis()==1)//accelerator only supports Y axis at the moment
|
||||||
|
)
|
||||||
|
{
|
||||||
|
///optimized version for btHeightfieldTerrainShape
|
||||||
|
btHeightfieldTerrainShape* heightField = (btHeightfieldTerrainShape*)collisionShape;
|
||||||
|
btTransform worldTocollisionObject = colObjWorldTransform.inverse();
|
||||||
|
btVector3 rayFromLocal = worldTocollisionObject * rayFromTrans.getOrigin();
|
||||||
|
btVector3 rayToLocal = worldTocollisionObject * rayToTrans.getOrigin();
|
||||||
|
|
||||||
|
BridgeTriangleRaycastCallback rcb(rayFromLocal, rayToLocal, &resultCallback, collisionObjectWrap->getCollisionObject(), heightField, colObjWorldTransform);
|
||||||
|
rcb.m_hitFraction = resultCallback.m_closestHitFraction;
|
||||||
|
heightField->performRaycast(&rcb, rayFromLocal, rayToLocal);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
//generic (slower) case
|
//generic (slower) case
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ void btHeightfieldTerrainShape::initialize(
|
|||||||
m_upAxis = upAxis;
|
m_upAxis = upAxis;
|
||||||
m_localScaling.setValue(btScalar(1.), btScalar(1.), btScalar(1.));
|
m_localScaling.setValue(btScalar(1.), btScalar(1.), btScalar(1.));
|
||||||
|
|
||||||
|
m_vboundsChunkSize = 0;
|
||||||
|
m_vboundsGridWidth = 0;
|
||||||
|
m_vboundsGridLength = 0;
|
||||||
|
|
||||||
// determine min/max axis-aligned bounding box (aabb) values
|
// determine min/max axis-aligned bounding box (aabb) values
|
||||||
switch (m_upAxis)
|
switch (m_upAxis)
|
||||||
{
|
{
|
||||||
@@ -108,6 +112,7 @@ void btHeightfieldTerrainShape::initialize(
|
|||||||
|
|
||||||
btHeightfieldTerrainShape::~btHeightfieldTerrainShape()
|
btHeightfieldTerrainShape::~btHeightfieldTerrainShape()
|
||||||
{
|
{
|
||||||
|
clearAccelerator();
|
||||||
}
|
}
|
||||||
|
|
||||||
void btHeightfieldTerrainShape::getAabb(const btTransform& t, btVector3& aabbMin, btVector3& aabbMax) const
|
void btHeightfieldTerrainShape::getAabb(const btTransform& t, btVector3& aabbMin, btVector3& aabbMax) const
|
||||||
@@ -323,6 +328,8 @@ void btHeightfieldTerrainShape::processAllTriangles(btTriangleCallback* callback
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO If m_vboundsGrid is available, use it to determine if we really need to process this area
|
||||||
|
|
||||||
for (int j = startJ; j < endJ; j++)
|
for (int j = startJ; j < endJ; j++)
|
||||||
{
|
{
|
||||||
for (int x = startX; x < endX; x++)
|
for (int x = startX; x < endX; x++)
|
||||||
@@ -373,3 +380,447 @@ const btVector3& btHeightfieldTerrainShape::getLocalScaling() const
|
|||||||
{
|
{
|
||||||
return m_localScaling;
|
return m_localScaling;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct GridRaycastState
|
||||||
|
{
|
||||||
|
int x; // Next quad coords
|
||||||
|
int z;
|
||||||
|
int prev_x; // Previous quad coords
|
||||||
|
int prev_z;
|
||||||
|
btScalar param; // Exit param for previous quad
|
||||||
|
btScalar prevParam; // Enter param for previous quad
|
||||||
|
btScalar maxDistanceFlat;
|
||||||
|
btScalar maxDistance3d;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Does it really need to take 3D vectors?
|
||||||
|
/// Iterates through a virtual 2D grid of unit-sized square cells,
|
||||||
|
/// and executes an action on each cell intersecting the given segment, ordered from begin to end.
|
||||||
|
/// Initially inspired by http://www.cse.yorku.ca/~amana/research/grid.pdf
|
||||||
|
template <typename Action_T>
|
||||||
|
void gridRaycast(Action_T& quadAction, const btVector3& beginPos, const btVector3& endPos)
|
||||||
|
{
|
||||||
|
GridRaycastState rs;
|
||||||
|
rs.maxDistance3d = beginPos.distance(endPos);
|
||||||
|
if (rs.maxDistance3d < 0.0001)
|
||||||
|
{
|
||||||
|
// Consider the ray is too small to hit anything
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btScalar rayDirectionFlatX = endPos[0] - beginPos[0];
|
||||||
|
btScalar rayDirectionFlatZ = endPos[2] - beginPos[2];
|
||||||
|
rs.maxDistanceFlat = btSqrt(rayDirectionFlatX * rayDirectionFlatX + rayDirectionFlatZ * rayDirectionFlatZ);
|
||||||
|
|
||||||
|
if (rs.maxDistanceFlat < 0.0001)
|
||||||
|
{
|
||||||
|
// Consider the ray vertical
|
||||||
|
rayDirectionFlatX = 0;
|
||||||
|
rayDirectionFlatZ = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rayDirectionFlatX /= rs.maxDistanceFlat;
|
||||||
|
rayDirectionFlatZ /= rs.maxDistanceFlat;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int xiStep = rayDirectionFlatX > 0 ? 1 : rayDirectionFlatX < 0 ? -1 : 0;
|
||||||
|
const int ziStep = rayDirectionFlatZ > 0 ? 1 : rayDirectionFlatZ < 0 ? -1 : 0;
|
||||||
|
|
||||||
|
const float infinite = 9999999;
|
||||||
|
const btScalar paramDeltaX = xiStep != 0 ? 1.f / btFabs(rayDirectionFlatX) : infinite;
|
||||||
|
const btScalar paramDeltaZ = ziStep != 0 ? 1.f / btFabs(rayDirectionFlatZ) : infinite;
|
||||||
|
|
||||||
|
// pos = param * dir
|
||||||
|
btScalar paramCrossX; // At which value of `param` we will cross a x-axis lane?
|
||||||
|
btScalar paramCrossZ; // At which value of `param` we will cross a z-axis lane?
|
||||||
|
|
||||||
|
// paramCrossX and paramCrossZ are initialized as being the first cross
|
||||||
|
// X initialization
|
||||||
|
if (xiStep != 0)
|
||||||
|
{
|
||||||
|
if (xiStep == 1)
|
||||||
|
{
|
||||||
|
paramCrossX = (ceil(beginPos[0]) - beginPos[0]) * paramDeltaX;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
paramCrossX = (beginPos[0] - floor(beginPos[0])) * paramDeltaX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
paramCrossX = infinite; // Will never cross on X
|
||||||
|
}
|
||||||
|
|
||||||
|
// Z initialization
|
||||||
|
if (ziStep != 0)
|
||||||
|
{
|
||||||
|
if (ziStep == 1)
|
||||||
|
{
|
||||||
|
paramCrossZ = (ceil(beginPos[2]) - beginPos[2]) * paramDeltaZ;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
paramCrossZ = (beginPos[2] - floor(beginPos[2])) * paramDeltaZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
paramCrossZ = infinite; // Will never cross on Z
|
||||||
|
}
|
||||||
|
|
||||||
|
rs.x = static_cast<int>(floor(beginPos[0]));
|
||||||
|
rs.z = static_cast<int>(floor(beginPos[2]));
|
||||||
|
|
||||||
|
// Workaround cases where the ray starts at an integer position
|
||||||
|
if (paramCrossX == 0.0)
|
||||||
|
{
|
||||||
|
paramCrossX += paramDeltaX;
|
||||||
|
// If going backwards, we should ignore the position we would get by the above flooring,
|
||||||
|
// because the ray is not heading in that direction
|
||||||
|
if (xiStep == -1)
|
||||||
|
{
|
||||||
|
rs.x -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paramCrossZ == 0.0)
|
||||||
|
{
|
||||||
|
paramCrossZ += paramDeltaZ;
|
||||||
|
if (ziStep == -1)
|
||||||
|
rs.z -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
rs.prev_x = rs.x;
|
||||||
|
rs.prev_z = rs.z;
|
||||||
|
rs.param = 0;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
rs.prev_x = rs.x;
|
||||||
|
rs.prev_z = rs.z;
|
||||||
|
rs.prevParam = rs.param;
|
||||||
|
|
||||||
|
if (paramCrossX < paramCrossZ)
|
||||||
|
{
|
||||||
|
// X lane
|
||||||
|
rs.x += xiStep;
|
||||||
|
// Assign before advancing the param,
|
||||||
|
// to be in sync with the initialization step
|
||||||
|
rs.param = paramCrossX;
|
||||||
|
paramCrossX += paramDeltaX;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Z lane
|
||||||
|
rs.z += ziStep;
|
||||||
|
rs.param = paramCrossZ;
|
||||||
|
paramCrossZ += paramDeltaZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rs.param > rs.maxDistanceFlat)
|
||||||
|
{
|
||||||
|
rs.param = rs.maxDistanceFlat;
|
||||||
|
quadAction(rs);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
quadAction(rs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProcessTrianglesAction
|
||||||
|
{
|
||||||
|
const btHeightfieldTerrainShape* shape;
|
||||||
|
bool flipQuadEdges;
|
||||||
|
bool useDiamondSubdivision;
|
||||||
|
int width;
|
||||||
|
int length;
|
||||||
|
btTriangleCallback* callback;
|
||||||
|
|
||||||
|
void exec(int x, int z) const
|
||||||
|
{
|
||||||
|
if (x < 0 || z < 0 || x >= width || z >= length)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btVector3 vertices[3];
|
||||||
|
|
||||||
|
// TODO Since this is for raycasts, we could greatly benefit from an early exit on the first hit
|
||||||
|
|
||||||
|
// Check quad
|
||||||
|
if (flipQuadEdges || (useDiamondSubdivision && (((z + x) & 1) > 0)))
|
||||||
|
{
|
||||||
|
// First triangle
|
||||||
|
shape->getVertex(x, z, vertices[0]);
|
||||||
|
shape->getVertex(x + 1, z, vertices[1]);
|
||||||
|
shape->getVertex(x + 1, z + 1, vertices[2]);
|
||||||
|
callback->processTriangle(vertices, x, z);
|
||||||
|
|
||||||
|
// Second triangle
|
||||||
|
shape->getVertex(x, z, vertices[0]);
|
||||||
|
shape->getVertex(x + 1, z + 1, vertices[1]);
|
||||||
|
shape->getVertex(x, z + 1, vertices[2]);
|
||||||
|
callback->processTriangle(vertices, x, z);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// First triangle
|
||||||
|
shape->getVertex(x, z, vertices[0]);
|
||||||
|
shape->getVertex(x, z + 1, vertices[1]);
|
||||||
|
shape->getVertex(x + 1, z, vertices[2]);
|
||||||
|
callback->processTriangle(vertices, x, z);
|
||||||
|
|
||||||
|
// Second triangle
|
||||||
|
shape->getVertex(x + 1, z, vertices[0]);
|
||||||
|
shape->getVertex(x, z + 1, vertices[1]);
|
||||||
|
shape->getVertex(x + 1, z + 1, vertices[2]);
|
||||||
|
callback->processTriangle(vertices, x, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void operator()(const GridRaycastState& bs) const
|
||||||
|
{
|
||||||
|
exec(bs.prev_x, bs.prev_z);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ProcessVBoundsAction
|
||||||
|
{
|
||||||
|
const btAlignedObjectArray<btHeightfieldTerrainShape::Range>& vbounds;
|
||||||
|
int width;
|
||||||
|
int length;
|
||||||
|
int chunkSize;
|
||||||
|
|
||||||
|
btVector3 rayBegin;
|
||||||
|
btVector3 rayEnd;
|
||||||
|
btVector3 rayDir;
|
||||||
|
|
||||||
|
ProcessTrianglesAction processTriangles;
|
||||||
|
|
||||||
|
ProcessVBoundsAction(const btAlignedObjectArray<btHeightfieldTerrainShape::Range>& bnd)
|
||||||
|
: vbounds(bnd)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
void operator()(const GridRaycastState& rs) const
|
||||||
|
{
|
||||||
|
int x = rs.prev_x;
|
||||||
|
int z = rs.prev_z;
|
||||||
|
|
||||||
|
if (x < 0 || z < 0 || x >= width || z >= length)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btHeightfieldTerrainShape::Range chunk = vbounds[x + z * width];
|
||||||
|
|
||||||
|
btVector3 enterPos;
|
||||||
|
btVector3 exitPos;
|
||||||
|
|
||||||
|
if (rs.maxDistanceFlat > 0.0001)
|
||||||
|
{
|
||||||
|
btScalar flatTo3d = chunkSize * rs.maxDistance3d / rs.maxDistanceFlat;
|
||||||
|
btScalar enterParam3d = rs.prevParam * flatTo3d;
|
||||||
|
btScalar exitParam3d = rs.param * flatTo3d;
|
||||||
|
enterPos = rayBegin + rayDir * enterParam3d;
|
||||||
|
exitPos = rayBegin + rayDir * exitParam3d;
|
||||||
|
|
||||||
|
// We did enter the flat projection of the AABB,
|
||||||
|
// but we have to check if we intersect it on the vertical axis
|
||||||
|
if (enterPos[1] > chunk.max && exitPos[1] > chunk.max)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (enterPos[1] < chunk.min && exitPos[1] < chunk.min)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Consider the ray vertical
|
||||||
|
// (though we shouldn't reach this often because there is an early check up-front)
|
||||||
|
enterPos = rayBegin;
|
||||||
|
exitPos = rayEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
gridRaycast(processTriangles, enterPos, exitPos);
|
||||||
|
// Note: it could be possible to have more than one grid at different levels,
|
||||||
|
// to do this there would be a branch using a pointer to another ProcessVBoundsAction
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO How do I interrupt the ray when there is a hit? `callback` does not return any result
|
||||||
|
/// Performs a raycast using a hierarchical Bresenham algorithm.
|
||||||
|
/// Does not allocate any memory by itself.
|
||||||
|
void btHeightfieldTerrainShape::performRaycast(btTriangleCallback* callback, const btVector3& raySource, const btVector3& rayTarget) const
|
||||||
|
{
|
||||||
|
// Transform to cell-local
|
||||||
|
btVector3 beginPos = raySource / m_localScaling;
|
||||||
|
btVector3 endPos = rayTarget / m_localScaling;
|
||||||
|
beginPos += m_localOrigin;
|
||||||
|
endPos += m_localOrigin;
|
||||||
|
|
||||||
|
ProcessTrianglesAction processTriangles;
|
||||||
|
processTriangles.shape = this;
|
||||||
|
processTriangles.flipQuadEdges = m_flipQuadEdges;
|
||||||
|
processTriangles.useDiamondSubdivision = m_useDiamondSubdivision;
|
||||||
|
processTriangles.callback = callback;
|
||||||
|
processTriangles.width = m_heightStickWidth - 1;
|
||||||
|
processTriangles.length = m_heightStickLength - 1;
|
||||||
|
|
||||||
|
// TODO Transform vectors to account for m_upAxis
|
||||||
|
int iBeginX = static_cast<int>(floor(beginPos[0]));
|
||||||
|
int iBeginZ = static_cast<int>(floor(beginPos[2]));
|
||||||
|
int iEndX = static_cast<int>(floor(endPos[0]));
|
||||||
|
int iEndZ = static_cast<int>(floor(endPos[2]));
|
||||||
|
|
||||||
|
if (iBeginX == iEndX && iBeginZ == iEndZ)
|
||||||
|
{
|
||||||
|
// The ray will never cross quads within the plane,
|
||||||
|
// so directly process triangles within one quad
|
||||||
|
// (typically, vertical rays should end up here)
|
||||||
|
processTriangles.exec(iBeginX, iEndZ);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_vboundsGrid.size()==0)
|
||||||
|
{
|
||||||
|
// Process all quads intersecting the flat projection of the ray
|
||||||
|
gridRaycast(processTriangles, beginPos, endPos);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
btVector3 rayDiff = endPos - beginPos;
|
||||||
|
btScalar flatDistance2 = rayDiff[0] * rayDiff[0] + rayDiff[2] * rayDiff[2];
|
||||||
|
if (flatDistance2 < m_vboundsChunkSize * m_vboundsChunkSize)
|
||||||
|
{
|
||||||
|
// Don't use chunks, the ray is too short in the plane
|
||||||
|
gridRaycast(processTriangles, beginPos, endPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessVBoundsAction processVBounds(m_vboundsGrid);
|
||||||
|
processVBounds.width = m_vboundsGridWidth;
|
||||||
|
processVBounds.length = m_vboundsGridLength;
|
||||||
|
processVBounds.rayBegin = beginPos;
|
||||||
|
processVBounds.rayEnd = endPos;
|
||||||
|
processVBounds.rayDir = rayDiff.normalized();
|
||||||
|
processVBounds.processTriangles = processTriangles;
|
||||||
|
processVBounds.chunkSize = m_vboundsChunkSize;
|
||||||
|
// The ray is long, run raycast on a higher-level grid
|
||||||
|
gridRaycast(processVBounds, beginPos / m_vboundsChunkSize, endPos / m_vboundsChunkSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a grid data structure storing the min and max heights of the terrain in chunks.
|
||||||
|
/// if chunkSize is zero, that accelerator is removed.
|
||||||
|
/// If you modify the heights, you need to rebuild this accelerator.
|
||||||
|
void btHeightfieldTerrainShape::buildAccelerator(int chunkSize)
|
||||||
|
{
|
||||||
|
if (chunkSize <= 0)
|
||||||
|
{
|
||||||
|
clearAccelerator();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_vboundsChunkSize = chunkSize;
|
||||||
|
int nChunksX = m_heightStickWidth / chunkSize;
|
||||||
|
int nChunksZ = m_heightStickLength / chunkSize;
|
||||||
|
|
||||||
|
if (m_heightStickWidth % chunkSize > 0)
|
||||||
|
{
|
||||||
|
++nChunksX; // In case terrain size isn't dividable by chunk size
|
||||||
|
}
|
||||||
|
if (m_heightStickLength % chunkSize > 0)
|
||||||
|
{
|
||||||
|
++nChunksZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_vboundsGridWidth != nChunksX || m_vboundsGridLength != nChunksZ)
|
||||||
|
{
|
||||||
|
clearAccelerator();
|
||||||
|
m_vboundsGridWidth = nChunksX;
|
||||||
|
m_vboundsGridLength = nChunksZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nChunksX == 0 || nChunksZ == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This data structure is only reallocated if the required size changed
|
||||||
|
m_vboundsGrid.resize(nChunksX * nChunksZ);
|
||||||
|
|
||||||
|
// Compute min and max height for all chunks
|
||||||
|
for (int cz = 0; cz < nChunksZ; ++cz)
|
||||||
|
{
|
||||||
|
int z0 = cz * chunkSize;
|
||||||
|
|
||||||
|
for (int cx = 0; cx < nChunksX; ++cx)
|
||||||
|
{
|
||||||
|
int x0 = cx * chunkSize;
|
||||||
|
|
||||||
|
Range r;
|
||||||
|
|
||||||
|
r.min = getRawHeightFieldValue(x0, z0);
|
||||||
|
r.max = r.min;
|
||||||
|
|
||||||
|
// Compute min and max height for this chunk.
|
||||||
|
// We have to include one extra cell to account for neighbors.
|
||||||
|
// Here is why:
|
||||||
|
// Say we have a flat terrain, and a plateau that fits a chunk perfectly.
|
||||||
|
//
|
||||||
|
// Left Right
|
||||||
|
// 0---0---0---1---1---1
|
||||||
|
// | | | | | |
|
||||||
|
// 0---0---0---1---1---1
|
||||||
|
// | | | | | |
|
||||||
|
// 0---0---0---1---1---1
|
||||||
|
// x
|
||||||
|
//
|
||||||
|
// If the AABB for the Left chunk did not share vertices with the Right,
|
||||||
|
// then we would fail collision tests at x due to a gap.
|
||||||
|
//
|
||||||
|
for (int z = z0; z < z0 + chunkSize + 1; ++z)
|
||||||
|
{
|
||||||
|
if (z >= m_heightStickLength)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int x = x0; x < x0 + chunkSize + 1; ++x)
|
||||||
|
{
|
||||||
|
if (x >= m_heightStickWidth)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
btScalar height = getRawHeightFieldValue(x, z);
|
||||||
|
|
||||||
|
if (height < r.min)
|
||||||
|
{
|
||||||
|
r.min = height;
|
||||||
|
}
|
||||||
|
else if (height > r.max)
|
||||||
|
{
|
||||||
|
r.max = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_vboundsGrid[cx + cz * nChunksX] = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void btHeightfieldTerrainShape::clearAccelerator()
|
||||||
|
{
|
||||||
|
m_vboundsGrid.clear();
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ subject to the following restrictions:
|
|||||||
#define BT_HEIGHTFIELD_TERRAIN_SHAPE_H
|
#define BT_HEIGHTFIELD_TERRAIN_SHAPE_H
|
||||||
|
|
||||||
#include "btConcaveShape.h"
|
#include "btConcaveShape.h"
|
||||||
|
#include "LinearMath/btAlignedObjectArray.h"
|
||||||
|
|
||||||
///btHeightfieldTerrainShape simulates a 2D heightfield terrain
|
///btHeightfieldTerrainShape simulates a 2D heightfield terrain
|
||||||
/**
|
/**
|
||||||
@@ -71,6 +72,13 @@ subject to the following restrictions:
|
|||||||
ATTRIBUTE_ALIGNED16(class)
|
ATTRIBUTE_ALIGNED16(class)
|
||||||
btHeightfieldTerrainShape : public btConcaveShape
|
btHeightfieldTerrainShape : public btConcaveShape
|
||||||
{
|
{
|
||||||
|
public:
|
||||||
|
struct Range
|
||||||
|
{
|
||||||
|
btScalar min;
|
||||||
|
btScalar max;
|
||||||
|
};
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
btVector3 m_localAabbMin;
|
btVector3 m_localAabbMin;
|
||||||
btVector3 m_localAabbMax;
|
btVector3 m_localAabbMax;
|
||||||
@@ -100,9 +108,14 @@ protected:
|
|||||||
|
|
||||||
btVector3 m_localScaling;
|
btVector3 m_localScaling;
|
||||||
|
|
||||||
|
// Accelerator
|
||||||
|
btAlignedObjectArray<Range> m_vboundsGrid;
|
||||||
|
int m_vboundsGridWidth;
|
||||||
|
int m_vboundsGridLength;
|
||||||
|
int m_vboundsChunkSize;
|
||||||
|
|
||||||
virtual btScalar getRawHeightFieldValue(int x, int y) const;
|
virtual btScalar getRawHeightFieldValue(int x, int y) const;
|
||||||
void quantizeWithClamp(int* out, const btVector3& point, int isMax) const;
|
void quantizeWithClamp(int* out, const btVector3& point, int isMax) const;
|
||||||
void getVertex(int x, int y, btVector3& vertex) const;
|
|
||||||
|
|
||||||
/// protected initialization
|
/// protected initialization
|
||||||
/**
|
/**
|
||||||
@@ -155,6 +168,17 @@ public:
|
|||||||
|
|
||||||
virtual const btVector3& getLocalScaling() const;
|
virtual const btVector3& getLocalScaling() const;
|
||||||
|
|
||||||
|
void getVertex(int x, int y, btVector3& vertex) const;
|
||||||
|
|
||||||
|
void performRaycast(btTriangleCallback * callback, const btVector3& raySource, const btVector3& rayTarget) const;
|
||||||
|
|
||||||
|
void buildAccelerator(int chunkSize = 16);
|
||||||
|
void clearAccelerator();
|
||||||
|
|
||||||
|
int getUpAxis() const
|
||||||
|
{
|
||||||
|
return m_upAxis;
|
||||||
|
}
|
||||||
//debugging
|
//debugging
|
||||||
virtual const char* getName() const { return "HEIGHTFIELD"; }
|
virtual const char* getName() const { return "HEIGHTFIELD"; }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ public:
|
|||||||
///SubSimplexConvexCastRaytest is the default, even if kF_None is set.
|
///SubSimplexConvexCastRaytest is the default, even if kF_None is set.
|
||||||
kF_UseSubSimplexConvexCastRaytest = 1 << 2, // Uses an approximate but faster ray versus convex intersection algorithm
|
kF_UseSubSimplexConvexCastRaytest = 1 << 2, // Uses an approximate but faster ray versus convex intersection algorithm
|
||||||
kF_UseGjkConvexCastRaytest = 1 << 3,
|
kF_UseGjkConvexCastRaytest = 1 << 3,
|
||||||
|
kF_DisableHeightfieldAccelerator = 1 << 4, //don't use the heightfield raycast accelerator. See https://github.com/bulletphysics/bullet3/pull/2062
|
||||||
kF_Terminator = 0xFFFFFFFF
|
kF_Terminator = 0xFFFFFFFF
|
||||||
};
|
};
|
||||||
unsigned int m_flags;
|
unsigned int m_flags;
|
||||||
|
|||||||
Reference in New Issue
Block a user