Merge branch 'master' into careminster
Conflicts: OpenSim/Region/Physics/BulletSPlugin/BSShapeCollection.csavinationmerge
commit
95d0a7d4fa
|
@ -33,6 +33,7 @@ using System.Threading;
|
||||||
using OpenSim.Framework;
|
using OpenSim.Framework;
|
||||||
using OpenSim.Framework.Capabilities;
|
using OpenSim.Framework.Capabilities;
|
||||||
using OpenSim.Framework.Client;
|
using OpenSim.Framework.Client;
|
||||||
|
using OpenSim.Framework.Monitoring;
|
||||||
using OpenSim.Region.Framework.Interfaces;
|
using OpenSim.Region.Framework.Interfaces;
|
||||||
using OpenSim.Region.Framework.Scenes;
|
using OpenSim.Region.Framework.Scenes;
|
||||||
using OpenSim.Region.Physics.Manager;
|
using OpenSim.Region.Physics.Manager;
|
||||||
|
@ -77,6 +78,31 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool DisableInterRegionTeleportCancellation { get; set; }
|
public bool DisableInterRegionTeleportCancellation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of times inter-region teleport was attempted.
|
||||||
|
/// </summary>
|
||||||
|
private Stat m_interRegionTeleportAttempts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of times inter-region teleport was aborted (due to simultaneous client logout).
|
||||||
|
/// </summary>
|
||||||
|
private Stat m_interRegionTeleportAborts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of times inter-region teleport was successfully cancelled by the client.
|
||||||
|
/// </summary>
|
||||||
|
private Stat m_interRegionTeleportCancels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of times inter-region teleport failed due to server/client/network problems (e.g. viewer failed to
|
||||||
|
/// connect with destination region).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is not necessarily a problem for this simulator - in open-grid/hg conditions, viewer connectivity to
|
||||||
|
/// destination simulator is unknown.
|
||||||
|
/// </remarks>
|
||||||
|
private Stat m_interRegionTeleportFailures;
|
||||||
|
|
||||||
protected bool m_Enabled = false;
|
protected bool m_Enabled = false;
|
||||||
|
|
||||||
public Scene Scene { get; private set; }
|
public Scene Scene { get; private set; }
|
||||||
|
@ -156,6 +182,60 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
|
||||||
|
|
||||||
Scene = scene;
|
Scene = scene;
|
||||||
|
|
||||||
|
m_interRegionTeleportAttempts =
|
||||||
|
new Stat(
|
||||||
|
"InterRegionTeleportAttempts",
|
||||||
|
"Number of inter-region teleports attempted.",
|
||||||
|
"This does not count attempts which failed due to pre-conditions (e.g. target simulator refused access).\n"
|
||||||
|
+ "You can get successfully teleports by subtracting aborts, cancels and teleport failures from this figure.",
|
||||||
|
"",
|
||||||
|
"entitytransfer",
|
||||||
|
Scene.Name,
|
||||||
|
StatType.Push,
|
||||||
|
null,
|
||||||
|
StatVerbosity.Debug);
|
||||||
|
|
||||||
|
m_interRegionTeleportAborts =
|
||||||
|
new Stat(
|
||||||
|
"InterRegionTeleportAborts",
|
||||||
|
"Number of inter-region teleports aborted due to client actions.",
|
||||||
|
"The chief action is simultaneous logout whilst teleporting.",
|
||||||
|
"",
|
||||||
|
"entitytransfer",
|
||||||
|
Scene.Name,
|
||||||
|
StatType.Push,
|
||||||
|
null,
|
||||||
|
StatVerbosity.Debug);
|
||||||
|
|
||||||
|
m_interRegionTeleportCancels =
|
||||||
|
new Stat(
|
||||||
|
"InterRegionTeleportCancels",
|
||||||
|
"Number of inter-region teleports cancelled by the client.",
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
"entitytransfer",
|
||||||
|
Scene.Name,
|
||||||
|
StatType.Push,
|
||||||
|
null,
|
||||||
|
StatVerbosity.Debug);
|
||||||
|
|
||||||
|
m_interRegionTeleportFailures =
|
||||||
|
new Stat(
|
||||||
|
"InterRegionTeleportFailures",
|
||||||
|
"Number of inter-region teleports that failed due to server/client/network issues.",
|
||||||
|
"This number may not be very helpful in open-grid/hg situations as the network connectivity/quality of destinations is uncontrollable.",
|
||||||
|
"",
|
||||||
|
"entitytransfer",
|
||||||
|
Scene.Name,
|
||||||
|
StatType.Push,
|
||||||
|
null,
|
||||||
|
StatVerbosity.Debug);
|
||||||
|
|
||||||
|
StatsManager.RegisterStat(m_interRegionTeleportAttempts);
|
||||||
|
StatsManager.RegisterStat(m_interRegionTeleportAborts);
|
||||||
|
StatsManager.RegisterStat(m_interRegionTeleportCancels);
|
||||||
|
StatsManager.RegisterStat(m_interRegionTeleportFailures);
|
||||||
|
|
||||||
scene.RegisterModuleInterface<IEntityTransferModule>(this);
|
scene.RegisterModuleInterface<IEntityTransferModule>(this);
|
||||||
scene.EventManager.OnNewClient += OnNewClient;
|
scene.EventManager.OnNewClient += OnNewClient;
|
||||||
}
|
}
|
||||||
|
@ -173,7 +253,13 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
|
||||||
|
|
||||||
public virtual void Close() {}
|
public virtual void Close() {}
|
||||||
|
|
||||||
public virtual void RemoveRegion(Scene scene) {}
|
public virtual void RemoveRegion(Scene scene)
|
||||||
|
{
|
||||||
|
StatsManager.DeregisterStat(m_interRegionTeleportAttempts);
|
||||||
|
StatsManager.DeregisterStat(m_interRegionTeleportAborts);
|
||||||
|
StatsManager.DeregisterStat(m_interRegionTeleportCancels);
|
||||||
|
StatsManager.DeregisterStat(m_interRegionTeleportFailures);
|
||||||
|
}
|
||||||
|
|
||||||
public virtual void RegionLoaded(Scene scene)
|
public virtual void RegionLoaded(Scene scene)
|
||||||
{
|
{
|
||||||
|
@ -548,6 +634,11 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Before this point, teleport 'failure' is due to checkable pre-conditions such as whether the target
|
||||||
|
// simulator can be found and is explicitly prepared to allow access. Therefore, we will not count these
|
||||||
|
// as server attempts.
|
||||||
|
m_interRegionTeleportAttempts.Value++;
|
||||||
|
|
||||||
m_log.DebugFormat("[ENTITY TRANSFER MODULE]: Destination is running version {0}", version);
|
m_log.DebugFormat("[ENTITY TRANSFER MODULE]: Destination is running version {0}", version);
|
||||||
|
|
||||||
// Fixing a bug where teleporting while sitting results in the avatar ending up removed from
|
// Fixing a bug where teleporting while sitting results in the avatar ending up removed from
|
||||||
|
@ -600,6 +691,8 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
|
||||||
bool logout = false;
|
bool logout = false;
|
||||||
if (!CreateAgent(sp, reg, finalDestination, agentCircuit, teleportFlags, out reason, out logout))
|
if (!CreateAgent(sp, reg, finalDestination, agentCircuit, teleportFlags, out reason, out logout))
|
||||||
{
|
{
|
||||||
|
m_interRegionTeleportFailures.Value++;
|
||||||
|
|
||||||
sp.ControllingClient.SendTeleportFailed(String.Format("Teleport refused: {0}", reason));
|
sp.ControllingClient.SendTeleportFailed(String.Format("Teleport refused: {0}", reason));
|
||||||
|
|
||||||
m_log.DebugFormat(
|
m_log.DebugFormat(
|
||||||
|
@ -611,6 +704,8 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
|
||||||
|
|
||||||
if (m_entityTransferStateMachine.GetAgentTransferState(sp.UUID) == AgentTransferState.Cancelling)
|
if (m_entityTransferStateMachine.GetAgentTransferState(sp.UUID) == AgentTransferState.Cancelling)
|
||||||
{
|
{
|
||||||
|
m_interRegionTeleportCancels.Value++;
|
||||||
|
|
||||||
m_log.DebugFormat(
|
m_log.DebugFormat(
|
||||||
"[ENTITY TRANSFER MODULE]: Cancelled teleport of {0} to {1} from {2} after CreateAgent on client request",
|
"[ENTITY TRANSFER MODULE]: Cancelled teleport of {0} to {1} from {2} after CreateAgent on client request",
|
||||||
sp.Name, finalDestination.RegionName, sp.Scene.Name);
|
sp.Name, finalDestination.RegionName, sp.Scene.Name);
|
||||||
|
@ -619,6 +714,8 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
|
||||||
}
|
}
|
||||||
else if (m_entityTransferStateMachine.GetAgentTransferState(sp.UUID) == AgentTransferState.Aborting)
|
else if (m_entityTransferStateMachine.GetAgentTransferState(sp.UUID) == AgentTransferState.Aborting)
|
||||||
{
|
{
|
||||||
|
m_interRegionTeleportAborts.Value++;
|
||||||
|
|
||||||
m_log.DebugFormat(
|
m_log.DebugFormat(
|
||||||
"[ENTITY TRANSFER MODULE]: Aborted teleport of {0} to {1} from {2} after CreateAgent due to previous client close.",
|
"[ENTITY TRANSFER MODULE]: Aborted teleport of {0} to {1} from {2} after CreateAgent due to previous client close.",
|
||||||
sp.Name, finalDestination.RegionName, sp.Scene.Name);
|
sp.Name, finalDestination.RegionName, sp.Scene.Name);
|
||||||
|
@ -688,6 +785,8 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
|
||||||
// establish th econnection to the destination which makes it return true.
|
// establish th econnection to the destination which makes it return true.
|
||||||
if (m_entityTransferStateMachine.GetAgentTransferState(sp.UUID) == AgentTransferState.Aborting)
|
if (m_entityTransferStateMachine.GetAgentTransferState(sp.UUID) == AgentTransferState.Aborting)
|
||||||
{
|
{
|
||||||
|
m_interRegionTeleportAborts.Value++;
|
||||||
|
|
||||||
m_log.DebugFormat(
|
m_log.DebugFormat(
|
||||||
"[ENTITY TRANSFER MODULE]: Aborted teleport of {0} to {1} from {2} before UpdateAgent",
|
"[ENTITY TRANSFER MODULE]: Aborted teleport of {0} to {1} from {2} before UpdateAgent",
|
||||||
sp.Name, finalDestination.RegionName, sp.Scene.Name);
|
sp.Name, finalDestination.RegionName, sp.Scene.Name);
|
||||||
|
@ -703,6 +802,8 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
|
||||||
{
|
{
|
||||||
if (m_entityTransferStateMachine.GetAgentTransferState(sp.UUID) == AgentTransferState.Aborting)
|
if (m_entityTransferStateMachine.GetAgentTransferState(sp.UUID) == AgentTransferState.Aborting)
|
||||||
{
|
{
|
||||||
|
m_interRegionTeleportAborts.Value++;
|
||||||
|
|
||||||
m_log.DebugFormat(
|
m_log.DebugFormat(
|
||||||
"[ENTITY TRANSFER MODULE]: Aborted teleport of {0} to {1} from {2} after UpdateAgent due to previous client close.",
|
"[ENTITY TRANSFER MODULE]: Aborted teleport of {0} to {1} from {2} after UpdateAgent due to previous client close.",
|
||||||
sp.Name, finalDestination.RegionName, sp.Scene.Name);
|
sp.Name, finalDestination.RegionName, sp.Scene.Name);
|
||||||
|
@ -720,6 +821,8 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
|
||||||
|
|
||||||
if (m_entityTransferStateMachine.GetAgentTransferState(sp.UUID) == AgentTransferState.Cancelling)
|
if (m_entityTransferStateMachine.GetAgentTransferState(sp.UUID) == AgentTransferState.Cancelling)
|
||||||
{
|
{
|
||||||
|
m_interRegionTeleportCancels.Value++;
|
||||||
|
|
||||||
m_log.DebugFormat(
|
m_log.DebugFormat(
|
||||||
"[ENTITY TRANSFER MODULE]: Cancelled teleport of {0} to {1} from {2} after UpdateAgent on client request",
|
"[ENTITY TRANSFER MODULE]: Cancelled teleport of {0} to {1} from {2} after UpdateAgent on client request",
|
||||||
sp.Name, finalDestination.RegionName, sp.Scene.Name);
|
sp.Name, finalDestination.RegionName, sp.Scene.Name);
|
||||||
|
@ -755,6 +858,8 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
|
||||||
{
|
{
|
||||||
if (m_entityTransferStateMachine.GetAgentTransferState(sp.UUID) == AgentTransferState.Aborting)
|
if (m_entityTransferStateMachine.GetAgentTransferState(sp.UUID) == AgentTransferState.Aborting)
|
||||||
{
|
{
|
||||||
|
m_interRegionTeleportAborts.Value++;
|
||||||
|
|
||||||
m_log.DebugFormat(
|
m_log.DebugFormat(
|
||||||
"[ENTITY TRANSFER MODULE]: Aborted teleport of {0} to {1} from {2} after WaitForAgentArrivedAtDestination due to previous client close.",
|
"[ENTITY TRANSFER MODULE]: Aborted teleport of {0} to {1} from {2} after WaitForAgentArrivedAtDestination due to previous client close.",
|
||||||
sp.Name, finalDestination.RegionName, sp.Scene.Name);
|
sp.Name, finalDestination.RegionName, sp.Scene.Name);
|
||||||
|
@ -767,6 +872,7 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
|
||||||
sp.Name, finalDestination.RegionName, sp.Scene.RegionInfo.RegionName);
|
sp.Name, finalDestination.RegionName, sp.Scene.RegionInfo.RegionName);
|
||||||
|
|
||||||
Fail(sp, finalDestination, logout, "Destination region did not signal teleport completion.");
|
Fail(sp, finalDestination, logout, "Destination region did not signal teleport completion.");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -808,15 +914,6 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
|
||||||
// now we have a child agent in this region.
|
// now we have a child agent in this region.
|
||||||
sp.Reset();
|
sp.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commented pending deletion since this method no longer appears to do anything at all
|
|
||||||
// // REFACTORING PROBLEM. Well, not a problem, but this method is HORRIBLE!
|
|
||||||
// if (sp.Scene.NeedSceneCacheClear(sp.UUID))
|
|
||||||
// {
|
|
||||||
// m_log.DebugFormat(
|
|
||||||
// "[ENTITY TRANSFER MODULE]: User {0} is going to another region, profile cache removed",
|
|
||||||
// sp.UUID);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -852,6 +949,8 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
|
||||||
{
|
{
|
||||||
CleanupFailedInterRegionTeleport(sp, finalDestination);
|
CleanupFailedInterRegionTeleport(sp, finalDestination);
|
||||||
|
|
||||||
|
m_interRegionTeleportFailures.Value++;
|
||||||
|
|
||||||
sp.ControllingClient.SendTeleportFailed(
|
sp.ControllingClient.SendTeleportFailed(
|
||||||
string.Format(
|
string.Format(
|
||||||
"Problems connecting to destination {0}, reason: {1}", finalDestination.RegionName, reason));
|
"Problems connecting to destination {0}, reason: {1}", finalDestination.RegionName, reason));
|
||||||
|
|
|
@ -321,7 +321,7 @@ namespace OpenSim.Region.Physics.BulletSPlugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void ProcessTypeChange(Vehicle pType)
|
public void ProcessTypeChange(Vehicle pType)
|
||||||
{
|
{
|
||||||
VDetailLog("{0},ProcessTypeChange,type={1}", Prim.LocalID, pType);
|
VDetailLog("{0},ProcessTypeChange,type={1}", Prim.LocalID, pType);
|
||||||
// Set Defaults For Type
|
// Set Defaults For Type
|
||||||
|
@ -1301,14 +1301,52 @@ namespace OpenSim.Region.Physics.BulletSPlugin
|
||||||
// efficiency of 1.0 will cause the spring to reach its equilibrium with exponential decay.
|
// efficiency of 1.0 will cause the spring to reach its equilibrium with exponential decay.
|
||||||
public void ComputeAngularVerticalAttraction()
|
public void ComputeAngularVerticalAttraction()
|
||||||
{
|
{
|
||||||
|
|
||||||
// If vertical attaction timescale is reasonable
|
// If vertical attaction timescale is reasonable
|
||||||
if (enableAngularVerticalAttraction && m_verticalAttractionTimescale < m_verticalAttractionCutoff)
|
if (enableAngularVerticalAttraction && m_verticalAttractionTimescale < m_verticalAttractionCutoff)
|
||||||
{
|
{
|
||||||
|
// Possible solution derived from a discussion at:
|
||||||
|
// http://stackoverflow.com/questions/14939657/computing-vector-from-quaternion-works-computing-quaternion-from-vector-does-no
|
||||||
|
|
||||||
|
// Create a rotation that is only the vehicle's rotation around Z
|
||||||
|
Vector3 currentEuler = Vector3.Zero;
|
||||||
|
VehicleOrientation.GetEulerAngles(out currentEuler.X, out currentEuler.Y, out currentEuler.Z);
|
||||||
|
Quaternion justZOrientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, currentEuler.Z);
|
||||||
|
|
||||||
|
// Create the axis that is perpendicular to the up vector and the rotated up vector.
|
||||||
|
Vector3 differenceAxis = Vector3.Cross(Vector3.UnitZ * justZOrientation, Vector3.UnitZ * VehicleOrientation);
|
||||||
|
// Compute the angle between those to vectors.
|
||||||
|
double differenceAngle = Math.Acos((double)Vector3.Dot(Vector3.UnitZ, Vector3.Normalize(Vector3.UnitZ * VehicleOrientation)));
|
||||||
|
// 'differenceAngle' is the angle to rotate and 'differenceAxis' is the plane to rotate in to get the vehicle vertical
|
||||||
|
|
||||||
|
// Reduce the change by the time period it is to change in. Timestep is handled when velocity is applied.
|
||||||
|
// TODO: add 'efficiency'.
|
||||||
|
differenceAngle /= m_verticalAttractionTimescale;
|
||||||
|
|
||||||
|
// Create the quaterian representing the correction angle
|
||||||
|
Quaternion correctionRotation = Quaternion.CreateFromAxisAngle(differenceAxis, (float)differenceAngle);
|
||||||
|
|
||||||
|
// Turn that quaternion into Euler values to make it into velocities to apply.
|
||||||
|
Vector3 vertContributionV = Vector3.Zero;
|
||||||
|
correctionRotation.GetEulerAngles(out vertContributionV.X, out vertContributionV.Y, out vertContributionV.Z);
|
||||||
|
vertContributionV *= -1f;
|
||||||
|
|
||||||
|
VehicleRotationalVelocity += vertContributionV;
|
||||||
|
|
||||||
|
VDetailLog("{0}, MoveAngular,verticalAttraction,diffAxis={1},diffAng={2},corrRot={3},contrib={4}",
|
||||||
|
Prim.LocalID,
|
||||||
|
differenceAxis,
|
||||||
|
differenceAngle,
|
||||||
|
correctionRotation,
|
||||||
|
vertContributionV);
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
/*
|
||||||
Vector3 vertContributionV = Vector3.Zero;
|
Vector3 vertContributionV = Vector3.Zero;
|
||||||
Vector3 origRotVelW = VehicleRotationalVelocity; // DEBUG DEBUG
|
Vector3 origRotVelW = VehicleRotationalVelocity; // DEBUG DEBUG
|
||||||
|
|
||||||
// Take a vector pointing up and convert it from world to vehicle relative coords.
|
// Take a vector pointing up and convert it from world to vehicle relative coords.
|
||||||
Vector3 verticalError = Vector3.UnitZ * VehicleOrientation;
|
Vector3 verticalError = Vector3.Normalize(Vector3.UnitZ * VehicleOrientation);
|
||||||
|
|
||||||
// If vertical attraction correction is needed, the vector that was pointing up (UnitZ)
|
// If vertical attraction correction is needed, the vector that was pointing up (UnitZ)
|
||||||
// is now:
|
// is now:
|
||||||
|
@ -1334,13 +1372,17 @@ namespace OpenSim.Region.Physics.BulletSPlugin
|
||||||
// 'vertContrbution' is now the necessary angular correction to correct tilt in one second.
|
// 'vertContrbution' is now the necessary angular correction to correct tilt in one second.
|
||||||
// Correction happens over a number of seconds.
|
// Correction happens over a number of seconds.
|
||||||
Vector3 unscaledContribVerticalErrorV = vertContributionV; // DEBUG DEBUG
|
Vector3 unscaledContribVerticalErrorV = vertContributionV; // DEBUG DEBUG
|
||||||
|
|
||||||
|
// The correction happens over the user's time period
|
||||||
vertContributionV /= m_verticalAttractionTimescale;
|
vertContributionV /= m_verticalAttractionTimescale;
|
||||||
|
|
||||||
VehicleRotationalVelocity += vertContributionV;
|
// Rotate the vehicle rotation to the world coordinates.
|
||||||
|
VehicleRotationalVelocity += (vertContributionV * VehicleOrientation);
|
||||||
|
|
||||||
VDetailLog("{0}, MoveAngular,verticalAttraction,,origRotVW={1},vertError={2},unscaledV={3},eff={4},ts={5},vertContribV={6}",
|
VDetailLog("{0}, MoveAngular,verticalAttraction,,origRotVW={1},vertError={2},unscaledV={3},eff={4},ts={5},vertContribV={6}",
|
||||||
Prim.LocalID, origRotVelW, verticalError, unscaledContribVerticalErrorV,
|
Prim.LocalID, origRotVelW, verticalError, unscaledContribVerticalErrorV,
|
||||||
m_verticalAttractionEfficiency, m_verticalAttractionTimescale, vertContributionV);
|
m_verticalAttractionEfficiency, m_verticalAttractionTimescale, vertContributionV);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -142,6 +142,14 @@ public static class BSParam
|
||||||
public static float VehicleAngularBankingTimescaleFudge { get; private set; }
|
public static float VehicleAngularBankingTimescaleFudge { get; private set; }
|
||||||
public static bool VehicleDebuggingEnabled { get; private set; }
|
public static bool VehicleDebuggingEnabled { get; private set; }
|
||||||
|
|
||||||
|
// Convex Hulls
|
||||||
|
public static int CSHullMaxDepthSplit { get; private set; }
|
||||||
|
public static int CSHullMaxDepthSplitForSimpleShapes { get; private set; }
|
||||||
|
public static float CSHullConcavityThresholdPercent { get; private set; }
|
||||||
|
public static float CSHullVolumeConservationThresholdPercent { get; private set; }
|
||||||
|
public static int CSHullMaxVertices { get; private set; }
|
||||||
|
public static float CSHullMaxSkinWidth { get; private set; }
|
||||||
|
|
||||||
// Linkset implementation parameters
|
// Linkset implementation parameters
|
||||||
public static float LinksetImplementation { get; private set; }
|
public static float LinksetImplementation { get; private set; }
|
||||||
public static bool LinkConstraintUseFrameOffset { get; private set; }
|
public static bool LinkConstraintUseFrameOffset { get; private set; }
|
||||||
|
@ -195,10 +203,10 @@ public static class BSParam
|
||||||
public delegate void PSetOnObject<T>(BSScene scene, BSPhysObject obj);
|
public delegate void PSetOnObject<T>(BSScene scene, BSPhysObject obj);
|
||||||
public sealed class ParameterDefn<T> : ParameterDefnBase
|
public sealed class ParameterDefn<T> : ParameterDefnBase
|
||||||
{
|
{
|
||||||
T defaultValue;
|
private T defaultValue;
|
||||||
PSetValue<T> setter;
|
private PSetValue<T> setter;
|
||||||
PGetValue<T> getter;
|
private PGetValue<T> getter;
|
||||||
PSetOnObject<T> objectSet;
|
private PSetOnObject<T> objectSet;
|
||||||
public ParameterDefn(string pName, string pDesc, T pDefault, PGetValue<T> pGetter, PSetValue<T> pSetter)
|
public ParameterDefn(string pName, string pDesc, T pDefault, PGetValue<T> pGetter, PSetValue<T> pSetter)
|
||||||
: base(pName, pDesc)
|
: base(pName, pDesc)
|
||||||
{
|
{
|
||||||
|
@ -215,13 +223,23 @@ public static class BSParam
|
||||||
getter = pGetter;
|
getter = pGetter;
|
||||||
objectSet = pObjSetter;
|
objectSet = pObjSetter;
|
||||||
}
|
}
|
||||||
|
/* Wish I could simplify using this definition but CLR doesn't store references so closure around delegates of references won't work
|
||||||
|
public ParameterDefn(string pName, string pDesc, T pDefault, ref T loc)
|
||||||
|
: base(pName, pDesc)
|
||||||
|
{
|
||||||
|
defaultValue = pDefault;
|
||||||
|
setter = (s, v) => { loc = v; };
|
||||||
|
getter = (s) => { return loc; };
|
||||||
|
objectSet = null;
|
||||||
|
}
|
||||||
|
*/
|
||||||
public override void AssignDefault(BSScene s)
|
public override void AssignDefault(BSScene s)
|
||||||
{
|
{
|
||||||
setter(s, defaultValue);
|
setter(s, defaultValue);
|
||||||
}
|
}
|
||||||
public override string GetValue(BSScene s)
|
public override string GetValue(BSScene s)
|
||||||
{
|
{
|
||||||
return String.Format("{0}", getter(s));
|
return getter(s).ToString();
|
||||||
}
|
}
|
||||||
public override void SetValue(BSScene s, string valAsString)
|
public override void SetValue(BSScene s, string valAsString)
|
||||||
{
|
{
|
||||||
|
@ -244,6 +262,7 @@ public static class BSParam
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
T setValue = (T)parser.Invoke(genericType, new Object[] { valAsString });
|
T setValue = (T)parser.Invoke(genericType, new Object[] { valAsString });
|
||||||
|
// Store the parsed value
|
||||||
setter(s, setValue);
|
setter(s, setValue);
|
||||||
// s.Logger.DebugFormat("{0} Parameter {1} = {2}", LogHeader, name, setValue);
|
// s.Logger.DebugFormat("{0} Parameter {1} = {2}", LogHeader, name, setValue);
|
||||||
}
|
}
|
||||||
|
@ -623,6 +642,31 @@ public static class BSParam
|
||||||
(s) => { return GlobalContactBreakingThreshold; },
|
(s) => { return GlobalContactBreakingThreshold; },
|
||||||
(s,v) => { GlobalContactBreakingThreshold = v; s.UnmanagedParams[0].globalContactBreakingThreshold = v; } ),
|
(s,v) => { GlobalContactBreakingThreshold = v; s.UnmanagedParams[0].globalContactBreakingThreshold = v; } ),
|
||||||
|
|
||||||
|
new ParameterDefn<int>("CSHullMaxDepthSplit", "CS impl: max depth to split for hull. 1-10 but > 7 is iffy",
|
||||||
|
7,
|
||||||
|
(s) => { return CSHullMaxDepthSplit; },
|
||||||
|
(s,v) => { CSHullMaxDepthSplit = v; } ),
|
||||||
|
new ParameterDefn<int>("CSHullMaxDepthSplitForSimpleShapes", "CS impl: max depth setting for simple prim shapes",
|
||||||
|
2,
|
||||||
|
(s) => { return CSHullMaxDepthSplitForSimpleShapes; },
|
||||||
|
(s,v) => { CSHullMaxDepthSplitForSimpleShapes = v; } ),
|
||||||
|
new ParameterDefn<float>("CSHullConcavityThresholdPercent", "CS impl: concavity threshold percent (0-20)",
|
||||||
|
5f,
|
||||||
|
(s) => { return CSHullConcavityThresholdPercent; },
|
||||||
|
(s,v) => { CSHullConcavityThresholdPercent = v; } ),
|
||||||
|
new ParameterDefn<float>("CSHullVolumeConservationThresholdPercent", "percent volume conservation to collapse hulls (0-30)",
|
||||||
|
5f,
|
||||||
|
(s) => { return CSHullVolumeConservationThresholdPercent; },
|
||||||
|
(s,v) => { CSHullVolumeConservationThresholdPercent = v; } ),
|
||||||
|
new ParameterDefn<int>("CSHullMaxVertices", "CS impl: maximum number of vertices in output hulls. Keep < 50.",
|
||||||
|
32,
|
||||||
|
(s) => { return CSHullMaxVertices; },
|
||||||
|
(s,v) => { CSHullMaxVertices = v; } ),
|
||||||
|
new ParameterDefn<float>("CSHullMaxSkinWidth", "CS impl: skin width to apply to output hulls.",
|
||||||
|
0,
|
||||||
|
(s) => { return CSHullMaxSkinWidth; },
|
||||||
|
(s,v) => { CSHullMaxSkinWidth = v; } ),
|
||||||
|
|
||||||
new ParameterDefn<float>("LinksetImplementation", "Type of linkset implementation (0=Constraint, 1=Compound, 2=Manual)",
|
new ParameterDefn<float>("LinksetImplementation", "Type of linkset implementation (0=Constraint, 1=Compound, 2=Manual)",
|
||||||
(float)BSLinkset.LinksetImplementation.Compound,
|
(float)BSLinkset.LinksetImplementation.Compound,
|
||||||
(s) => { return LinksetImplementation; },
|
(s) => { return LinksetImplementation; },
|
||||||
|
|
|
@ -86,7 +86,7 @@ public abstract class BSPhysObject : PhysicsActor
|
||||||
PhysBody = new BulletBody(localID);
|
PhysBody = new BulletBody(localID);
|
||||||
PhysShape = new BulletShape();
|
PhysShape = new BulletShape();
|
||||||
|
|
||||||
LastAssetBuildFailed = false;
|
PrimAssetState = PrimAssetCondition.Unknown;
|
||||||
|
|
||||||
// Default material type. Also sets Friction, Restitution and Density.
|
// Default material type. Also sets Friction, Restitution and Density.
|
||||||
SetMaterial((int)MaterialAttributes.Material.Wood);
|
SetMaterial((int)MaterialAttributes.Material.Wood);
|
||||||
|
@ -133,9 +133,13 @@ public abstract class BSPhysObject : PhysicsActor
|
||||||
// Reference to the physical shape (btCollisionShape) of this object
|
// Reference to the physical shape (btCollisionShape) of this object
|
||||||
public BulletShape PhysShape;
|
public BulletShape PhysShape;
|
||||||
|
|
||||||
// 'true' if the mesh's underlying asset failed to build.
|
// The physical representation of the prim might require an asset fetch.
|
||||||
// This will keep us from looping after the first time the build failed.
|
// The asset state is first 'Unknown' then 'Waiting' then either 'Failed' or 'Fetched'.
|
||||||
public bool LastAssetBuildFailed { get; set; }
|
public enum PrimAssetCondition
|
||||||
|
{
|
||||||
|
Unknown, Waiting, Failed, Fetched
|
||||||
|
}
|
||||||
|
public PrimAssetCondition PrimAssetState { get; set; }
|
||||||
|
|
||||||
// The objects base shape information. Null if not a prim type shape.
|
// The objects base shape information. Null if not a prim type shape.
|
||||||
public PrimitiveBaseShape BaseShape { get; protected set; }
|
public PrimitiveBaseShape BaseShape { get; protected set; }
|
||||||
|
|
|
@ -155,7 +155,7 @@ public class BSPrim : BSPhysObject
|
||||||
public override PrimitiveBaseShape Shape {
|
public override PrimitiveBaseShape Shape {
|
||||||
set {
|
set {
|
||||||
BaseShape = value;
|
BaseShape = value;
|
||||||
LastAssetBuildFailed = false;
|
PrimAssetState = PrimAssetCondition.Unknown;
|
||||||
ForceBodyShapeRebuild(false);
|
ForceBodyShapeRebuild(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -447,17 +447,10 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
|
|
||||||
// If the prim attributes are simple, this could be a simple Bullet native shape
|
// If the prim attributes are simple, this could be a simple Bullet native shape
|
||||||
if (!haveShape
|
if (!haveShape
|
||||||
|
&& nativeShapePossible
|
||||||
&& pbs != null
|
&& pbs != null
|
||||||
&& !pbs.SculptEntry
|
&& !pbs.SculptEntry
|
||||||
&& nativeShapePossible
|
&& ((pbs.SculptEntry && !BSParam.ShouldMeshSculptedPrim) || PrimHasNoCuts(pbs)) )
|
||||||
&& ((pbs.SculptEntry && !BSParam.ShouldMeshSculptedPrim)
|
|
||||||
|| (pbs.ProfileBegin == 0 && pbs.ProfileEnd == 0
|
|
||||||
&& pbs.ProfileHollow == 0
|
|
||||||
&& pbs.PathTwist == 0 && pbs.PathTwistBegin == 0
|
|
||||||
&& pbs.PathBegin == 0 && pbs.PathEnd == 0
|
|
||||||
&& pbs.PathTaperX == 0 && pbs.PathTaperY == 0
|
|
||||||
&& pbs.PathScaleX == 100 && pbs.PathScaleY == 100
|
|
||||||
&& pbs.PathShearX == 0 && pbs.PathShearY == 0) ) )
|
|
||||||
{
|
{
|
||||||
// Get the scale of any existing shape so we can see if the new shape is same native type and same size.
|
// Get the scale of any existing shape so we can see if the new shape is same native type and same size.
|
||||||
OMV.Vector3 scaleOfExistingShape = OMV.Vector3.Zero;
|
OMV.Vector3 scaleOfExistingShape = OMV.Vector3.Zero;
|
||||||
|
@ -508,6 +501,18 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// return 'true' if this shape description does not include any cutting or twisting.
|
||||||
|
private bool PrimHasNoCuts(PrimitiveBaseShape pbs)
|
||||||
|
{
|
||||||
|
return pbs.ProfileBegin == 0 && pbs.ProfileEnd == 0
|
||||||
|
&& pbs.ProfileHollow == 0
|
||||||
|
&& pbs.PathTwist == 0 && pbs.PathTwistBegin == 0
|
||||||
|
&& pbs.PathBegin == 0 && pbs.PathEnd == 0
|
||||||
|
&& pbs.PathTaperX == 0 && pbs.PathTaperY == 0
|
||||||
|
&& pbs.PathScaleX == 100 && pbs.PathScaleY == 100
|
||||||
|
&& pbs.PathShearX == 0 && pbs.PathShearY == 0;
|
||||||
|
}
|
||||||
|
|
||||||
// return 'true' if the prim's shape was changed.
|
// return 'true' if the prim's shape was changed.
|
||||||
public bool CreateGeomMeshOrHull(BSPhysObject prim, ShapeDestructionCallback shapeCallback)
|
public bool CreateGeomMeshOrHull(BSPhysObject prim, ShapeDestructionCallback shapeCallback)
|
||||||
{
|
{
|
||||||
|
@ -518,7 +523,7 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
if (prim.IsPhysical && BSParam.ShouldUseHullsForPhysicalObjects)
|
if (prim.IsPhysical && BSParam.ShouldUseHullsForPhysicalObjects)
|
||||||
{
|
{
|
||||||
// Update prim.BSShape to reference a hull of this shape.
|
// Update prim.BSShape to reference a hull of this shape.
|
||||||
ret = GetReferenceToHull(prim,shapeCallback);
|
ret = GetReferenceToHull(prim, shapeCallback);
|
||||||
if (DDetail) DetailLog("{0},BSShapeCollection.CreateGeom,hull,shape={1},key={2}",
|
if (DDetail) DetailLog("{0},BSShapeCollection.CreateGeom,hull,shape={1},key={2}",
|
||||||
prim.LocalID, prim.PhysShape, prim.PhysShape.shapeKey.ToString("X"));
|
prim.LocalID, prim.PhysShape, prim.PhysShape.shapeKey.ToString("X"));
|
||||||
}
|
}
|
||||||
|
@ -699,6 +704,7 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
|
|
||||||
// See that hull shape exists in the physical world and update prim.BSShape.
|
// See that hull shape exists in the physical world and update prim.BSShape.
|
||||||
// We could be creating the hull because scale changed or whatever.
|
// We could be creating the hull because scale changed or whatever.
|
||||||
|
// Return 'true' if a new hull was built. Otherwise, returning a shared hull instance.
|
||||||
private bool GetReferenceToHull(BSPhysObject prim, ShapeDestructionCallback shapeCallback)
|
private bool GetReferenceToHull(BSPhysObject prim, ShapeDestructionCallback shapeCallback)
|
||||||
{
|
{
|
||||||
BulletShape newShape;
|
BulletShape newShape;
|
||||||
|
@ -717,6 +723,7 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
DereferenceShape(prim.PhysShape, 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);
|
||||||
|
// It might not have been created if we're waiting for an asset.
|
||||||
newShape = VerifyMeshCreated(newShape, prim);
|
newShape = VerifyMeshCreated(newShape, prim);
|
||||||
|
|
||||||
ReferenceShape(newShape);
|
ReferenceShape(newShape);
|
||||||
|
@ -735,13 +742,13 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
HullDesc hullDesc;
|
HullDesc hullDesc;
|
||||||
if (Hulls.TryGetValue(newHullKey, out hullDesc))
|
if (Hulls.TryGetValue(newHullKey, out hullDesc))
|
||||||
{
|
{
|
||||||
// If the hull shape already is created, just use it.
|
// If the hull shape already has been created, just use the one shared instance.
|
||||||
newShape = hullDesc.shape.Clone();
|
newShape = hullDesc.shape.Clone();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Build a new hull in the physical world
|
// Build a new hull in the physical world.
|
||||||
// Pass true for physicalness as this creates some sort of bounding box which we don't need
|
// Pass true for physicalness as this prevents the creation of bounding box which is not needed
|
||||||
IMesh meshData = PhysicsScene.mesher.CreateMesh(objName, pbs, size, lod, true, false, false, false);
|
IMesh meshData = PhysicsScene.mesher.CreateMesh(objName, pbs, size, lod, true, false, false, false);
|
||||||
if (meshData != null)
|
if (meshData != null)
|
||||||
{
|
{
|
||||||
|
@ -761,15 +768,35 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
convVertices.Add(new float3(vv.X, vv.Y, vv.Z));
|
convVertices.Add(new float3(vv.X, vv.Y, vv.Z));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint maxDepthSplit = (uint)BSParam.CSHullMaxDepthSplit;
|
||||||
|
if (BSParam.CSHullMaxDepthSplit != BSParam.CSHullMaxDepthSplitForSimpleShapes)
|
||||||
|
{
|
||||||
|
// Simple primitive shapes we know are convex so they are better implemented with
|
||||||
|
// fewer hulls.
|
||||||
|
// Check for simple shape (prim without cuts) and reduce split parameter if so.
|
||||||
|
if (PrimHasNoCuts(pbs))
|
||||||
|
{
|
||||||
|
maxDepthSplit = (uint)BSParam.CSHullMaxDepthSplitForSimpleShapes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// setup and do convex hull conversion
|
// setup and do convex hull conversion
|
||||||
m_hulls = new List<ConvexResult>();
|
m_hulls = new List<ConvexResult>();
|
||||||
DecompDesc dcomp = new DecompDesc();
|
DecompDesc dcomp = new DecompDesc();
|
||||||
dcomp.mIndices = convIndices;
|
dcomp.mIndices = convIndices;
|
||||||
dcomp.mVertices = convVertices;
|
dcomp.mVertices = convVertices;
|
||||||
|
dcomp.mDepth = maxDepthSplit;
|
||||||
|
dcomp.mCpercent = BSParam.CSHullConcavityThresholdPercent;
|
||||||
|
dcomp.mPpercent = BSParam.CSHullVolumeConservationThresholdPercent;
|
||||||
|
dcomp.mMaxVertices = (uint)BSParam.CSHullMaxVertices;
|
||||||
|
dcomp.mSkinWidth = BSParam.CSHullMaxSkinWidth;
|
||||||
ConvexBuilder convexBuilder = new ConvexBuilder(HullReturn);
|
ConvexBuilder convexBuilder = new ConvexBuilder(HullReturn);
|
||||||
// create the hull into the _hulls variable
|
// create the hull into the _hulls variable
|
||||||
convexBuilder.process(dcomp);
|
convexBuilder.process(dcomp);
|
||||||
|
|
||||||
|
DetailLog("{0},BSShapeCollection.CreatePhysicalHull,key={1},inVert={2},inInd={3},split={4},hulls={5}",
|
||||||
|
BSScene.DetailLogZero, newHullKey, indices.GetLength(0), vertices.Count, maxDepthSplit, m_hulls.Count);
|
||||||
|
|
||||||
// Convert the vertices and indices for passing to unmanaged.
|
// Convert the vertices and indices for passing to unmanaged.
|
||||||
// The hull information is passed as a large floating point array.
|
// The hull information is passed as a large floating point array.
|
||||||
// The format is:
|
// The format is:
|
||||||
|
@ -905,11 +932,15 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
return newShape;
|
return newShape;
|
||||||
|
|
||||||
// If this mesh has an underlying asset and we have not failed getting it before, fetch the asset
|
// If this mesh has an underlying asset and we have not failed getting it before, fetch the asset
|
||||||
if (prim.BaseShape.SculptEntry && !prim.LastAssetBuildFailed && prim.BaseShape.SculptTexture != OMV.UUID.Zero)
|
if (prim.BaseShape.SculptEntry
|
||||||
|
&& prim.PrimAssetState != BSPhysObject.PrimAssetCondition.Failed
|
||||||
|
&& prim.PrimAssetState != BSPhysObject.PrimAssetCondition.Waiting
|
||||||
|
&& prim.BaseShape.SculptTexture != OMV.UUID.Zero
|
||||||
|
)
|
||||||
{
|
{
|
||||||
DetailLog("{0},BSShapeCollection.VerifyMeshCreated,fetchAsset,lastFailed={1}", prim.LocalID, prim.LastAssetBuildFailed);
|
DetailLog("{0},BSShapeCollection.VerifyMeshCreated,fetchAsset", prim.LocalID);
|
||||||
// This will prevent looping through this code as we keep trying to get the failed shape
|
// Multiple requestors will know we're waiting for this asset
|
||||||
prim.LastAssetBuildFailed = true;
|
prim.PrimAssetState = BSPhysObject.PrimAssetCondition.Waiting;
|
||||||
|
|
||||||
BSPhysObject xprim = prim;
|
BSPhysObject xprim = prim;
|
||||||
Util.FireAndForget(delegate
|
Util.FireAndForget(delegate
|
||||||
|
@ -920,7 +951,7 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
BSPhysObject yprim = xprim; // probably not necessary, but, just in case.
|
BSPhysObject yprim = xprim; // probably not necessary, but, just in case.
|
||||||
assetProvider(yprim.BaseShape.SculptTexture, delegate(AssetBase asset)
|
assetProvider(yprim.BaseShape.SculptTexture, delegate(AssetBase asset)
|
||||||
{
|
{
|
||||||
bool assetFound = false; // DEBUG DEBUG
|
bool assetFound = false;
|
||||||
string mismatchIDs = String.Empty; // DEBUG DEBUG
|
string mismatchIDs = String.Empty; // DEBUG DEBUG
|
||||||
if (asset != null && yprim.BaseShape.SculptEntry)
|
if (asset != null && yprim.BaseShape.SculptEntry)
|
||||||
{
|
{
|
||||||
|
@ -938,6 +969,10 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
mismatchIDs = yprim.BaseShape.SculptTexture.ToString() + "/" + asset.ID;
|
mismatchIDs = yprim.BaseShape.SculptTexture.ToString() + "/" + asset.ID;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (assetFound)
|
||||||
|
yprim.PrimAssetState = BSPhysObject.PrimAssetCondition.Fetched;
|
||||||
|
else
|
||||||
|
yprim.PrimAssetState = BSPhysObject.PrimAssetCondition.Failed;
|
||||||
DetailLog("{0},BSShapeCollection,fetchAssetCallback,found={1},isSculpt={2},ids={3}",
|
DetailLog("{0},BSShapeCollection,fetchAssetCallback,found={1},isSculpt={2},ids={3}",
|
||||||
yprim.LocalID, assetFound, yprim.BaseShape.SculptEntry, mismatchIDs );
|
yprim.LocalID, assetFound, yprim.BaseShape.SculptEntry, mismatchIDs );
|
||||||
|
|
||||||
|
@ -945,6 +980,7 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
xprim.PrimAssetState = BSPhysObject.PrimAssetCondition.Failed;
|
||||||
PhysicsScene.Logger.ErrorFormat("{0} Physical object requires asset but no asset provider. Name={1}",
|
PhysicsScene.Logger.ErrorFormat("{0} Physical object requires asset but no asset provider. Name={1}",
|
||||||
LogHeader, PhysicsScene.Name);
|
LogHeader, PhysicsScene.Name);
|
||||||
}
|
}
|
||||||
|
@ -952,7 +988,7 @@ public sealed class BSShapeCollection : IDisposable
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (prim.LastAssetBuildFailed)
|
if (prim.PrimAssetState == BSPhysObject.PrimAssetCondition.Failed)
|
||||||
{
|
{
|
||||||
PhysicsScene.Logger.ErrorFormat("{0} Mesh failed to fetch asset. lID={1}, texture={2}",
|
PhysicsScene.Logger.ErrorFormat("{0} Mesh failed to fetch asset. lID={1}, texture={2}",
|
||||||
LogHeader, prim.LocalID, prim.BaseShape.SculptTexture);
|
LogHeader, prim.LocalID, prim.BaseShape.SculptTexture);
|
||||||
|
|
Loading…
Reference in New Issue