BulletSim: fix crash caused when linksets were rebuilt. A problem added
when individual child pos/rot changes were implementated a week or so ago. Remove some passing of inTaintTime flag when it was never false.user_profiles
parent
ed71c939fc
commit
75a05c16c5
|
@ -126,9 +126,9 @@ public sealed class BSCharacter : BSPhysObject
|
||||||
DetailLog("{0},BSCharacter.Destroy", LocalID);
|
DetailLog("{0},BSCharacter.Destroy", LocalID);
|
||||||
PhysicsScene.TaintedObject("BSCharacter.destroy", delegate()
|
PhysicsScene.TaintedObject("BSCharacter.destroy", delegate()
|
||||||
{
|
{
|
||||||
PhysicsScene.Shapes.DereferenceBody(PhysBody, true /* inTaintTime */, null /* bodyCallback */);
|
PhysicsScene.Shapes.DereferenceBody(PhysBody, null /* bodyCallback */);
|
||||||
PhysBody.Clear();
|
PhysBody.Clear();
|
||||||
PhysicsScene.Shapes.DereferenceShape(PhysShape, true /* inTaintTime */, null /* bodyCallback */);
|
PhysicsScene.Shapes.DereferenceShape(PhysShape, null /* bodyCallback */);
|
||||||
PhysShape.Clear();
|
PhysShape.Clear();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -219,28 +219,45 @@ public sealed class BSLinksetCompound : BSLinkset
|
||||||
{
|
{
|
||||||
// Gather the child info. It might not be there if the linkset is in transition.
|
// Gather the child info. It might not be there if the linkset is in transition.
|
||||||
BSLinksetCompoundInfo lsi = updated.LinksetInfo as BSLinksetCompoundInfo;
|
BSLinksetCompoundInfo lsi = updated.LinksetInfo as BSLinksetCompoundInfo;
|
||||||
|
|
||||||
|
// The linksetInfo will need to be rebuilt either here or when the linkset is rebuilt
|
||||||
if (LinksetRoot.PhysShape.HasPhysicalShape && lsi != null)
|
if (LinksetRoot.PhysShape.HasPhysicalShape && lsi != null)
|
||||||
{
|
{
|
||||||
if (PhysicsScene.PE.IsCompound(LinksetRoot.PhysShape))
|
if (PhysicsScene.PE.IsCompound(LinksetRoot.PhysShape))
|
||||||
{
|
{
|
||||||
BulletShape linksetChildShape = PhysicsScene.PE.GetChildShapeFromCompoundShapeIndex(LinksetRoot.PhysShape, lsi.Index);
|
int numLinksetChildren = PhysicsScene.PE.GetNumberOfCompoundChildren(LinksetRoot.PhysShape);
|
||||||
if (linksetChildShape.HasPhysicalShape)
|
if (lsi.Index < numLinksetChildren)
|
||||||
{
|
{
|
||||||
// Compute the offset from the center-of-gravity
|
// It is possible that the linkset is still under construction and the child is not yet
|
||||||
BSLinksetCompoundInfo newLsi = new BSLinksetCompoundInfo(lsi.Index, LinksetRoot, updated, LinksetRoot.PositionDisplacement);
|
// inserted into the compound shape. A rebuild of the linkset in a pre-step action will
|
||||||
PhysicsScene.PE.UpdateChildTransform(LinksetRoot.PhysShape, lsi.Index,
|
// build the whole thing with the new position or rotation.
|
||||||
newLsi.OffsetFromCenterOfMass,
|
// This must be checked for because Bullet references the child array but does no validity
|
||||||
newLsi.OffsetRot,
|
// checking of the child index passed.
|
||||||
true /* shouldRecalculateLocalAabb */);
|
BulletShape linksetChildShape = PhysicsScene.PE.GetChildShapeFromCompoundShapeIndex(LinksetRoot.PhysShape, lsi.Index);
|
||||||
DetailLog("{0},BSLinksetCompound.UpdateProperties,changeChildPosRot,whichUpdated={1},newLsi={2}",
|
if (linksetChildShape.HasPhysicalShape)
|
||||||
updated.LocalID, whichUpdated, newLsi);
|
{
|
||||||
updated.LinksetInfo = newLsi;
|
// Compute the offset from the center-of-gravity
|
||||||
updatedChild = true;
|
BSLinksetCompoundInfo newLsi = new BSLinksetCompoundInfo(lsi.Index, LinksetRoot, updated, LinksetRoot.PositionDisplacement);
|
||||||
|
PhysicsScene.PE.UpdateChildTransform(LinksetRoot.PhysShape, lsi.Index,
|
||||||
|
newLsi.OffsetFromCenterOfMass,
|
||||||
|
newLsi.OffsetRot,
|
||||||
|
true /* shouldRecalculateLocalAabb */);
|
||||||
|
updated.LinksetInfo = newLsi;
|
||||||
|
updatedChild = true;
|
||||||
|
DetailLog("{0},BSLinksetCompound.UpdateProperties,changeChildPosRot,whichUpdated={1},newLsi={2}",
|
||||||
|
updated.LocalID, whichUpdated, newLsi);
|
||||||
|
}
|
||||||
|
else // DEBUG DEBUG
|
||||||
|
{ // DEBUG DEBUG
|
||||||
|
DetailLog("{0},BSLinksetCompound.UpdateProperties,couldNotUpdateChild,noChildShape,shape={1}",
|
||||||
|
updated.LocalID, linksetChildShape);
|
||||||
|
} // DEBUG DEBUG
|
||||||
}
|
}
|
||||||
else // DEBUG DEBUG
|
else // DEBUG DEBUG
|
||||||
{ // DEBUG DEBUG
|
{ // DEBUG DEBUG
|
||||||
DetailLog("{0},BSLinksetCompound.UpdateProperties,couldNotUpdateChild,noChildShape,shape={1}",
|
// the child is not yet in the compound shape. This is non-fatal.
|
||||||
updated.LocalID, linksetChildShape);
|
DetailLog("{0},BSLinksetCompound.UpdateProperties,couldNotUpdateChild,childNotInCompoundShape,numChildren={1},index={2}",
|
||||||
|
updated.LocalID, numLinksetChildren, lsi.Index);
|
||||||
} // DEBUG DEBUG
|
} // DEBUG DEBUG
|
||||||
}
|
}
|
||||||
else // DEBUG DEBUG
|
else // DEBUG DEBUG
|
||||||
|
@ -256,6 +273,9 @@ public sealed class BSLinksetCompound : BSLinkset
|
||||||
if (!updatedChild)
|
if (!updatedChild)
|
||||||
{
|
{
|
||||||
// If couldn't do the individual child, the linkset needs a rebuild to incorporate the new child info.
|
// If couldn't do the individual child, the linkset needs a rebuild to incorporate the new child info.
|
||||||
|
// Note that there are several ways through this code that will not update the child that can
|
||||||
|
// occur if the linkset is being rebuilt. In this case, scheduling a rebuild is a NOOP since
|
||||||
|
// there will already be a rebuild scheduled.
|
||||||
DetailLog("{0},BSLinksetCompound.UpdateProperties,couldNotUpdateChild.schedulingRebuild,whichUpdated={1}",
|
DetailLog("{0},BSLinksetCompound.UpdateProperties,couldNotUpdateChild.schedulingRebuild,whichUpdated={1}",
|
||||||
updated.LocalID, whichUpdated);
|
updated.LocalID, whichUpdated);
|
||||||
updated.LinksetInfo = null; // setting to 'null' causes relative position to be recomputed.
|
updated.LinksetInfo = null; // setting to 'null' causes relative position to be recomputed.
|
||||||
|
|
|
@ -146,9 +146,9 @@ public sealed class BSPrim : BSPhysObject
|
||||||
{
|
{
|
||||||
DetailLog("{0},BSPrim.Destroy,taint,", LocalID);
|
DetailLog("{0},BSPrim.Destroy,taint,", LocalID);
|
||||||
// If there are physical body and shape, release my use of same.
|
// If there are physical body and shape, release my use of same.
|
||||||
PhysicsScene.Shapes.DereferenceBody(PhysBody, true, null);
|
PhysicsScene.Shapes.DereferenceBody(PhysBody, null);
|
||||||
PhysBody.Clear();
|
PhysBody.Clear();
|
||||||
PhysicsScene.Shapes.DereferenceShape(PhysShape, true, null);
|
PhysicsScene.Shapes.DereferenceShape(PhysShape, null);
|
||||||
PhysShape.Clear();
|
PhysShape.Clear();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -181,11 +181,19 @@ public sealed class BSPrim : BSPhysObject
|
||||||
|
|
||||||
public override bool ForceBodyShapeRebuild(bool inTaintTime)
|
public override bool ForceBodyShapeRebuild(bool inTaintTime)
|
||||||
{
|
{
|
||||||
PhysicsScene.TaintedObject(inTaintTime, "BSPrim.ForceBodyShapeRebuild", delegate()
|
if (inTaintTime)
|
||||||
{
|
{
|
||||||
_mass = CalculateMass(); // changing the shape changes the mass
|
_mass = CalculateMass(); // changing the shape changes the mass
|
||||||
CreateGeomAndObject(true);
|
CreateGeomAndObject(true);
|
||||||
});
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PhysicsScene.TaintedObject("BSPrim.ForceBodyShapeRebuild", delegate()
|
||||||
|
{
|
||||||
|
_mass = CalculateMass(); // changing the shape changes the mass
|
||||||
|
CreateGeomAndObject(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
public override bool Grabbed {
|
public override bool Grabbed {
|
||||||
|
|
|
@ -133,48 +133,44 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
// Track another user of a body.
|
// Track another user of a body.
|
||||||
// We presume the caller has allocated the body.
|
// We presume the caller has allocated the body.
|
||||||
// Bodies only have one user so the body is just put into the world if not already there.
|
// Bodies only have one user so the body is just put into the world if not already there.
|
||||||
public void ReferenceBody(BulletBody body, bool inTaintTime)
|
private void ReferenceBody(BulletBody body)
|
||||||
{
|
{
|
||||||
lock (m_collectionActivityLock)
|
lock (m_collectionActivityLock)
|
||||||
{
|
{
|
||||||
if (DDetail) DetailLog("{0},BSShapeCollection.ReferenceBody,newBody,body={1}", body.ID, body);
|
if (DDetail) DetailLog("{0},BSShapeCollection.ReferenceBody,newBody,body={1}", body.ID, body);
|
||||||
PhysicsScene.TaintedObject(inTaintTime, "BSShapeCollection.ReferenceBody", delegate()
|
if (!PhysicsScene.PE.IsInWorld(PhysicsScene.World, body))
|
||||||
{
|
{
|
||||||
if (!PhysicsScene.PE.IsInWorld(PhysicsScene.World, body))
|
PhysicsScene.PE.AddObjectToWorld(PhysicsScene.World, body);
|
||||||
{
|
if (DDetail) DetailLog("{0},BSShapeCollection.ReferenceBody,addedToWorld,ref={1}", body.ID, body);
|
||||||
PhysicsScene.PE.AddObjectToWorld(PhysicsScene.World, body);
|
}
|
||||||
if (DDetail) DetailLog("{0},BSShapeCollection.ReferenceBody,addedToWorld,ref={1}", body.ID, body);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release the usage of a body.
|
// Release the usage of a body.
|
||||||
// Called when releasing use of a BSBody. BSShape is handled separately.
|
// Called when releasing use of a BSBody. BSShape is handled separately.
|
||||||
public void DereferenceBody(BulletBody body, bool inTaintTime, BodyDestructionCallback bodyCallback )
|
// Called in taint time.
|
||||||
|
public void DereferenceBody(BulletBody body, BodyDestructionCallback bodyCallback )
|
||||||
{
|
{
|
||||||
if (!body.HasPhysicalBody)
|
if (!body.HasPhysicalBody)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
PhysicsScene.AssertInTaintTime("BSShapeCollection.DereferenceBody");
|
||||||
|
|
||||||
lock (m_collectionActivityLock)
|
lock (m_collectionActivityLock)
|
||||||
{
|
{
|
||||||
PhysicsScene.TaintedObject(inTaintTime, "BSShapeCollection.DereferenceBody", delegate()
|
if (DDetail) DetailLog("{0},BSShapeCollection.DereferenceBody,DestroyingBody,body={1}", body.ID, body);
|
||||||
|
// If the caller needs to know the old body is going away, pass the event up.
|
||||||
|
if (bodyCallback != null) bodyCallback(body);
|
||||||
|
|
||||||
|
if (PhysicsScene.PE.IsInWorld(PhysicsScene.World, body))
|
||||||
{
|
{
|
||||||
if (DDetail) DetailLog("{0},BSShapeCollection.DereferenceBody,DestroyingBody,body={1},inTaintTime={2}",
|
PhysicsScene.PE.RemoveObjectFromWorld(PhysicsScene.World, body);
|
||||||
body.ID, body, inTaintTime);
|
if (DDetail) DetailLog("{0},BSShapeCollection.DereferenceBody,removingFromWorld. Body={1}", body.ID, body);
|
||||||
// If the caller needs to know the old body is going away, pass the event up.
|
}
|
||||||
if (bodyCallback != null) bodyCallback(body);
|
|
||||||
|
|
||||||
if (PhysicsScene.PE.IsInWorld(PhysicsScene.World, body))
|
// Zero any reference to the shape so it is not freed when the body is deleted.
|
||||||
{
|
PhysicsScene.PE.SetCollisionShape(PhysicsScene.World, body, null);
|
||||||
PhysicsScene.PE.RemoveObjectFromWorld(PhysicsScene.World, body);
|
PhysicsScene.PE.DestroyObject(PhysicsScene.World, body);
|
||||||
if (DDetail) DetailLog("{0},BSShapeCollection.DereferenceBody,removingFromWorld. Body={1}", body.ID, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zero any reference to the shape so it is not freed when the body is deleted.
|
|
||||||
PhysicsScene.PE.SetCollisionShape(PhysicsScene.World, body, null);
|
|
||||||
PhysicsScene.PE.DestroyObject(PhysicsScene.World, body);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,44 +241,43 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release the usage of a shape.
|
// Release the usage of a shape.
|
||||||
public void DereferenceShape(BulletShape shape, bool inTaintTime, ShapeDestructionCallback shapeCallback)
|
public void DereferenceShape(BulletShape shape, ShapeDestructionCallback shapeCallback)
|
||||||
{
|
{
|
||||||
if (!shape.HasPhysicalShape)
|
if (!shape.HasPhysicalShape)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PhysicsScene.TaintedObject(inTaintTime, "BSShapeCollection.DereferenceShape", delegate()
|
PhysicsScene.AssertInTaintTime("BSShapeCollection.DereferenceShape");
|
||||||
|
|
||||||
|
if (shape.HasPhysicalShape)
|
||||||
{
|
{
|
||||||
if (shape.HasPhysicalShape)
|
if (shape.isNativeShape)
|
||||||
{
|
{
|
||||||
if (shape.isNativeShape)
|
// Native shapes are not tracked and are released immediately
|
||||||
|
if (DDetail) DetailLog("{0},BSShapeCollection.DereferenceShape,deleteNativeShape,ptr={1}",
|
||||||
|
BSScene.DetailLogZero, shape.AddrString);
|
||||||
|
if (shapeCallback != null) shapeCallback(shape);
|
||||||
|
PhysicsScene.PE.DeleteCollisionShape(PhysicsScene.World, shape);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (shape.type)
|
||||||
{
|
{
|
||||||
// Native shapes are not tracked and are released immediately
|
case BSPhysicsShapeType.SHAPE_HULL:
|
||||||
if (DDetail) DetailLog("{0},BSShapeCollection.DereferenceShape,deleteNativeShape,ptr={1},taintTime={2}",
|
DereferenceHull(shape, shapeCallback);
|
||||||
BSScene.DetailLogZero, shape.AddrString, inTaintTime);
|
break;
|
||||||
if (shapeCallback != null) shapeCallback(shape);
|
case BSPhysicsShapeType.SHAPE_MESH:
|
||||||
PhysicsScene.PE.DeleteCollisionShape(PhysicsScene.World, shape);
|
DereferenceMesh(shape, shapeCallback);
|
||||||
}
|
break;
|
||||||
else
|
case BSPhysicsShapeType.SHAPE_COMPOUND:
|
||||||
{
|
DereferenceCompound(shape, shapeCallback);
|
||||||
switch (shape.type)
|
break;
|
||||||
{
|
case BSPhysicsShapeType.SHAPE_UNKNOWN:
|
||||||
case BSPhysicsShapeType.SHAPE_HULL:
|
break;
|
||||||
DereferenceHull(shape, shapeCallback);
|
default:
|
||||||
break;
|
break;
|
||||||
case BSPhysicsShapeType.SHAPE_MESH:
|
|
||||||
DereferenceMesh(shape, shapeCallback);
|
|
||||||
break;
|
|
||||||
case BSPhysicsShapeType.SHAPE_COMPOUND:
|
|
||||||
DereferenceCompound(shape, shapeCallback);
|
|
||||||
break;
|
|
||||||
case BSPhysicsShapeType.SHAPE_UNKNOWN:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count down the reference count for a mesh shape
|
// Count down the reference count for a mesh shape
|
||||||
|
@ -393,7 +388,7 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
|
|
||||||
if (shapeInfo.type != BSPhysicsShapeType.SHAPE_UNKNOWN)
|
if (shapeInfo.type != BSPhysicsShapeType.SHAPE_UNKNOWN)
|
||||||
{
|
{
|
||||||
DereferenceShape(shapeInfo, true, null);
|
DereferenceShape(shapeInfo, null);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -543,7 +538,7 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
ShapeDestructionCallback shapeCallback)
|
ShapeDestructionCallback shapeCallback)
|
||||||
{
|
{
|
||||||
// release any previous shape
|
// release any previous shape
|
||||||
DereferenceShape(prim.PhysShape, true, shapeCallback);
|
DereferenceShape(prim.PhysShape, shapeCallback);
|
||||||
|
|
||||||
BulletShape newShape = BuildPhysicalNativeShape(prim, shapeType, shapeKey);
|
BulletShape newShape = BuildPhysicalNativeShape(prim, shapeType, shapeKey);
|
||||||
|
|
||||||
|
@ -611,7 +606,7 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
prim.LocalID, prim.PhysShape.shapeKey.ToString("X"), newMeshKey.ToString("X"));
|
prim.LocalID, prim.PhysShape.shapeKey.ToString("X"), newMeshKey.ToString("X"));
|
||||||
|
|
||||||
// Since we're recreating new, get rid of the reference to the previous shape
|
// Since we're recreating new, get rid of the reference to the previous shape
|
||||||
DereferenceShape(prim.PhysShape, true, shapeCallback);
|
DereferenceShape(prim.PhysShape, shapeCallback);
|
||||||
|
|
||||||
newShape = CreatePhysicalMesh(prim.PhysObjectName, newMeshKey, prim.BaseShape, prim.Size, lod);
|
newShape = CreatePhysicalMesh(prim.PhysObjectName, newMeshKey, prim.BaseShape, prim.Size, lod);
|
||||||
// Take evasive action if the mesh was not constructed.
|
// Take evasive action if the mesh was not constructed.
|
||||||
|
@ -682,7 +677,7 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
prim.LocalID, prim.PhysShape.shapeKey.ToString("X"), newHullKey.ToString("X"));
|
prim.LocalID, prim.PhysShape.shapeKey.ToString("X"), newHullKey.ToString("X"));
|
||||||
|
|
||||||
// Remove usage of the previous shape.
|
// Remove usage of the previous shape.
|
||||||
DereferenceShape(prim.PhysShape, true, shapeCallback);
|
DereferenceShape(prim.PhysShape, shapeCallback);
|
||||||
|
|
||||||
newShape = CreatePhysicalHull(prim.PhysObjectName, newHullKey, prim.BaseShape, prim.Size, lod);
|
newShape = CreatePhysicalHull(prim.PhysObjectName, newHullKey, prim.BaseShape, prim.Size, lod);
|
||||||
newShape = VerifyMeshCreated(newShape, prim);
|
newShape = VerifyMeshCreated(newShape, prim);
|
||||||
|
@ -817,7 +812,6 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
// Don't need to do this as the shape is freed when the new root shape is created below.
|
// Don't need to do this as the shape is freed when the new root shape is created below.
|
||||||
// DereferenceShape(prim.PhysShape, true, shapeCallback);
|
// DereferenceShape(prim.PhysShape, true, shapeCallback);
|
||||||
|
|
||||||
|
|
||||||
BulletShape cShape = PhysicsScene.PE.CreateCompoundShape(PhysicsScene.World, false);
|
BulletShape cShape = PhysicsScene.PE.CreateCompoundShape(PhysicsScene.World, false);
|
||||||
|
|
||||||
// Create the shape for the root prim and add it to the compound shape. Cannot be a native shape.
|
// Create the shape for the root prim and add it to the compound shape. Cannot be a native shape.
|
||||||
|
@ -956,7 +950,7 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
if (mustRebuild || forceRebuild)
|
if (mustRebuild || forceRebuild)
|
||||||
{
|
{
|
||||||
// Free any old body
|
// Free any old body
|
||||||
DereferenceBody(prim.PhysBody, true, bodyCallback);
|
DereferenceBody(prim.PhysBody, bodyCallback);
|
||||||
|
|
||||||
BulletBody aBody;
|
BulletBody aBody;
|
||||||
if (prim.IsSolid)
|
if (prim.IsSolid)
|
||||||
|
@ -970,7 +964,7 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
if (DDetail) DetailLog("{0},BSShapeCollection.CreateBody,ghost,body={1}", prim.LocalID, aBody);
|
if (DDetail) DetailLog("{0},BSShapeCollection.CreateBody,ghost,body={1}", prim.LocalID, aBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
ReferenceBody(aBody, true);
|
ReferenceBody(aBody);
|
||||||
|
|
||||||
prim.PhysBody = aBody;
|
prim.PhysBody = aBody;
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,10 @@ Vehicle attributes are not restored when a vehicle is rezzed on region creation
|
||||||
|
|
||||||
GENERAL TODO LIST:
|
GENERAL TODO LIST:
|
||||||
=================================================
|
=================================================
|
||||||
|
Collisions are inconsistant: arrows are supposed to hit and report collision. Often don't.
|
||||||
|
If arrow show at prim, collision reported about 1/3 of time. If collision reported,
|
||||||
|
both arrow and prim report it. The arrow bounces off the prim 9 out of 10 times.
|
||||||
|
Shooting 5m sphere "arrows" at 60m/s.
|
||||||
llMoveToTarget objects are not effected by gravity until target is removed.
|
llMoveToTarget objects are not effected by gravity until target is removed.
|
||||||
Compute CCD parameters based on body size
|
Compute CCD parameters based on body size
|
||||||
Can solver iterations be changed per body/shape? Can be for constraints but what
|
Can solver iterations be changed per body/shape? Can be for constraints but what
|
||||||
|
|
Loading…
Reference in New Issue