BSScene.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. /*
  2. * Copyright (c) Contributors, http://opensimulator.org/
  3. * See CONTRIBUTORS.TXT for a full list of copyright holders.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. * * Redistributions of source code must retain the above copyright
  8. * notice, this list of conditions and the following disclaimer.
  9. * * Redistributions in binary form must reproduce the above copyrightD
  10. * notice, this list of conditions and the following disclaimer in the
  11. * documentation and/or other materials provided with the distribution.
  12. * * Neither the name of the OpenSimulator Project nor the
  13. * names of its contributors may be used to endorse or promote products
  14. * derived from this software without specific prior written permission.
  15. *
  16. * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
  17. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19. * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
  20. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. */
  27. using System;
  28. using System.Collections.Generic;
  29. using System.Runtime.InteropServices;
  30. using System.Text;
  31. using System.Threading;
  32. using Nini.Config;
  33. using log4net;
  34. using OpenSim.Framework;
  35. using OpenSim.Region.Physics.Manager;
  36. using OpenMetaverse;
  37. using OpenSim.Region.Framework;
  38. // TODOs for BulletSim (for BSScene, BSPrim, BSCharacter and BulletSim)
  39. // Fix folding up feet
  40. // Fix terrain. Only flat terrain works. Terrain with shape is oriented wrong? Origined wrong?
  41. // Parameterize BulletSim. Pass a structure of parameters to the C++ code. Capsule size, friction, ...
  42. // Shift drag duplication of objects does not work
  43. // Adjust character capsule size when height is adjusted (ScenePresence.SetHeight)
  44. // Test sculpties
  45. // Compute physics FPS reasonably
  46. // Based on material, set density and friction
  47. // More efficient memory usage in passing hull information from BSPrim to BulletSim
  48. // Four states of prim: Physical, regular, phantom and selected. Are we modeling these correctly?
  49. // In SL one can set both physical and phantom (gravity, does not effect others, makes collisions with ground)
  50. // At the moment, physical and phantom causes object to drop through the terrain
  51. // Should prim.link() and prim.delink() membership checking happen at taint time?
  52. // Mesh sharing. Use meshHash to tell if we already have a hull of that shape and only create once
  53. // Do attachments need to be handled separately? Need collision events. Do not collide with VolumeDetect
  54. // Implement the genCollisions feature in BulletSim::SetObjectProperties (don't pass up unneeded collisions)
  55. // Implement LockAngularMotion
  56. // Decide if clearing forces is the right thing to do when setting position (BulletSim::SetObjectTranslation)
  57. // Built Galton board (lots of MoveTo's) and some slats were not positioned correctly (mistakes scattered)
  58. // No mistakes with ODE. Shape creation race condition?
  59. // Does NeedsMeshing() really need to exclude all the different shapes?
  60. //
  61. namespace OpenSim.Region.Physics.BulletSPlugin
  62. {
  63. public class BSScene : PhysicsScene
  64. {
  65. private static readonly ILog m_log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
  66. private static readonly string LogHeader = "[BULLETS SCENE]";
  67. private Dictionary<uint, BSCharacter> m_avatars = new Dictionary<uint, BSCharacter>();
  68. private Dictionary<uint, BSPrim> m_prims = new Dictionary<uint, BSPrim>();
  69. private List<BSPrim> m_vehicles = new List<BSPrim>();
  70. private float[] m_heightMap;
  71. private float m_waterLevel;
  72. private uint m_worldID;
  73. public uint WorldID { get { return m_worldID; } }
  74. public IMesher mesher;
  75. public int meshLOD = 32;
  76. private int m_maxSubSteps = 10;
  77. private float m_fixedTimeStep = 1f / 60f;
  78. private long m_simulationStep = 0;
  79. public long SimulationStep { get { return m_simulationStep; } }
  80. private bool _meshSculptedPrim = true; // cause scuplted prims to get meshed
  81. private bool _forceSimplePrimMeshing = false; // if a cube or sphere, let Bullet do internal shapes
  82. public float maximumMassObject = 10000.01f;
  83. public const uint TERRAIN_ID = 0;
  84. public const uint GROUNDPLANE_ID = 1;
  85. public float DefaultFriction = 0.70f;
  86. public float DefaultDensity = 10.000006836f; // Aluminum g/cm3; TODO: compute based on object material
  87. public Vector3 DefaultGravity = new Vector3(0, 0, -9.80665f);
  88. public delegate void TaintCallback();
  89. private List<TaintCallback> _taintedObjects;
  90. private Object _taintLock = new Object();
  91. private BulletSimAPI.DebugLogCallback debugLogCallbackHandle;
  92. public BSScene(string identifier)
  93. {
  94. }
  95. public override void Initialise(IMesher meshmerizer, IConfigSource config)
  96. {
  97. if (config != null)
  98. {
  99. IConfig pConfig = config.Configs["BulletSim"];
  100. if (pConfig != null)
  101. {
  102. DefaultFriction = pConfig.GetFloat("Friction", DefaultFriction);
  103. DefaultDensity = pConfig.GetFloat("Density", DefaultDensity);
  104. // TODO: a lot more parameters that are passed to BulletSim
  105. }
  106. }
  107. // if Debug, enable logging from the unmanaged code
  108. if (m_log.IsDebugEnabled)
  109. {
  110. m_log.DebugFormat("{0}: Initialize: Setting debug callback for unmanaged code", LogHeader);
  111. debugLogCallbackHandle = new BulletSimAPI.DebugLogCallback(BulletLogger);
  112. BulletSimAPI.SetDebugLogCallback(debugLogCallbackHandle);
  113. }
  114. _meshSculptedPrim = true; // mesh sculpted prims
  115. _forceSimplePrimMeshing = false; // use complex meshing if called for
  116. _taintedObjects = new List<TaintCallback>();
  117. mesher = meshmerizer;
  118. // m_log.DebugFormat("{0}: Initialize: Calling BulletSimAPI.Initialize.", LogHeader);
  119. m_worldID = BulletSimAPI.Initialize(new Vector3(Constants.RegionSize, Constants.RegionSize, 4096f));
  120. }
  121. // Called directly from unmanaged code so don't do much
  122. private void BulletLogger(string msg)
  123. {
  124. m_log.Debug("[BULLETS UNMANAGED]:" + msg);
  125. }
  126. public override PhysicsActor AddAvatar(string avName, Vector3 position, Vector3 size, bool isFlying)
  127. {
  128. m_log.ErrorFormat("{0}: CALL TO AddAvatar in BSScene. NOT IMPLEMENTED", LogHeader);
  129. return null;
  130. }
  131. public override PhysicsActor AddAvatar(uint localID, string avName, Vector3 position, Vector3 size, bool isFlying)
  132. {
  133. // m_log.DebugFormat("{0}: AddAvatar: {1}", LogHeader, avName);
  134. BSCharacter actor = new BSCharacter(localID, avName, this, position, size, isFlying);
  135. lock (m_avatars) m_avatars.Add(localID, actor);
  136. return actor;
  137. }
  138. public override void RemoveAvatar(PhysicsActor actor)
  139. {
  140. // m_log.DebugFormat("{0}: RemoveAvatar", LogHeader);
  141. if (actor is BSCharacter)
  142. {
  143. ((BSCharacter)actor).Destroy();
  144. }
  145. try
  146. {
  147. lock (m_avatars) m_avatars.Remove(actor.LocalID);
  148. }
  149. catch (Exception e)
  150. {
  151. m_log.WarnFormat("{0}: Attempt to remove avatar that is not in physics scene: {1}", LogHeader, e);
  152. }
  153. }
  154. public override void RemovePrim(PhysicsActor prim)
  155. {
  156. // m_log.DebugFormat("{0}: RemovePrim", LogHeader);
  157. if (prim is BSPrim)
  158. {
  159. ((BSPrim)prim).Destroy();
  160. }
  161. try
  162. {
  163. lock (m_prims) m_prims.Remove(prim.LocalID);
  164. }
  165. catch (Exception e)
  166. {
  167. m_log.WarnFormat("{0}: Attempt to remove prim that is not in physics scene: {1}", LogHeader, e);
  168. }
  169. }
  170. public override PhysicsActor AddPrimShape(string primName, PrimitiveBaseShape pbs, Vector3 position,
  171. Vector3 size, Quaternion rotation) // deprecated
  172. {
  173. return null;
  174. }
  175. public override PhysicsActor AddPrimShape(string primName, PrimitiveBaseShape pbs, Vector3 position,
  176. Vector3 size, Quaternion rotation, bool isPhysical)
  177. {
  178. m_log.ErrorFormat("{0}: CALL TO AddPrimShape in BSScene. NOT IMPLEMENTED", LogHeader);
  179. return null;
  180. }
  181. public override PhysicsActor AddPrimShape(uint localID, string primName, PrimitiveBaseShape pbs, Vector3 position,
  182. Vector3 size, Quaternion rotation, bool isPhysical)
  183. {
  184. // m_log.DebugFormat("{0}: AddPrimShape2: {1}", LogHeader, primName);
  185. IMesh mesh = null;
  186. if (NeedsMeshing(pbs))
  187. {
  188. // if the prim is complex, create the mesh for it.
  189. // If simple (box or sphere) leave 'mesh' null and physics will do a native shape.
  190. mesh = mesher.CreateMesh(primName, pbs, size, this.meshLOD, isPhysical);
  191. }
  192. BSPrim prim = new BSPrim(localID, primName, this, position, size, rotation, mesh, pbs, isPhysical);
  193. lock (m_prims) m_prims.Add(localID, prim);
  194. return prim;
  195. }
  196. // This is a call from the simulator saying that some physical property has been updated.
  197. // The BulletS driver senses the changing of relevant properties so this taint
  198. // information call is not needed.
  199. public override void AddPhysicsActorTaint(PhysicsActor prim) { }
  200. // Simulate one timestep
  201. public override float Simulate(float timeStep)
  202. {
  203. int updatedEntityCount;
  204. IntPtr updatedEntitiesPtr;
  205. IntPtr[] updatedEntities;
  206. int collidersCount;
  207. IntPtr collidersPtr;
  208. int[] colliders; // should be uint but Marshal.Copy does not have that overload
  209. // update the prim states while we know the physics engine is not busy
  210. ProcessTaints();
  211. // Some of the prims operate with special vehicle properties
  212. ProcessVehicles(timeStep);
  213. ProcessTaints(); // the vehicles might have added taints
  214. // step the physical world one interval
  215. m_simulationStep++;
  216. int numSubSteps = BulletSimAPI.PhysicsStep(m_worldID, timeStep, m_maxSubSteps, m_fixedTimeStep,
  217. out updatedEntityCount, out updatedEntitiesPtr, out collidersCount, out collidersPtr);
  218. // if there were collisions, they show up here
  219. if (collidersCount > 0)
  220. {
  221. colliders = new int[collidersCount];
  222. Marshal.Copy(collidersPtr, colliders, 0, collidersCount);
  223. for (int ii = 0; ii < collidersCount; ii+=2)
  224. {
  225. uint cA = (uint)colliders[ii];
  226. uint cB = (uint)colliders[ii+1];
  227. SendCollision(cA, cB);
  228. SendCollision(cB, cA);
  229. }
  230. }
  231. // if any of the objects had updated properties, they are returned in the updatedEntity structure
  232. // TODO: figure out how to pass all of the EntityProperties structures in one marshal call.
  233. if (updatedEntityCount > 0)
  234. {
  235. updatedEntities = new IntPtr[updatedEntityCount];
  236. // fetch all the pointers to all the EntityProperties structures for these updates
  237. Marshal.Copy(updatedEntitiesPtr, updatedEntities, 0, updatedEntityCount);
  238. for (int ii = 0; ii < updatedEntityCount; ii++)
  239. {
  240. IntPtr updatePointer = updatedEntities[ii];
  241. EntityProperties entprop = (EntityProperties)Marshal.PtrToStructure(updatePointer, typeof(EntityProperties));
  242. // m_log.DebugFormat("{0}: entprop: id={1}, pos={2}", LogHeader, entprop.ID, entprop.Position);
  243. BSCharacter actor;
  244. if (m_avatars.TryGetValue(entprop.ID, out actor))
  245. {
  246. actor.UpdateProperties(entprop);
  247. continue;
  248. }
  249. BSPrim prim;
  250. if (m_prims.TryGetValue(entprop.ID, out prim))
  251. {
  252. prim.UpdateProperties(entprop);
  253. }
  254. }
  255. }
  256. // fps calculation wrong. This calculation returns about 1 in normal operation.
  257. return timeStep / (numSubSteps * m_fixedTimeStep) * 1000f;
  258. }
  259. // Something has collided
  260. private void SendCollision(uint localID, uint collidingWith)
  261. {
  262. if (localID == TERRAIN_ID || localID == GROUNDPLANE_ID)
  263. {
  264. // we never send collisions to the terrain
  265. return;
  266. }
  267. ActorTypes type = ActorTypes.Prim;
  268. if (collidingWith == TERRAIN_ID || collidingWith == GROUNDPLANE_ID)
  269. type = ActorTypes.Ground;
  270. else if (m_avatars.ContainsKey(collidingWith))
  271. type = ActorTypes.Agent;
  272. BSPrim prim;
  273. if (m_prims.TryGetValue(localID, out prim)) {
  274. prim.Collide(collidingWith, type, Vector3.Zero, Vector3.UnitZ, 0.01f);
  275. return;
  276. }
  277. BSCharacter actor;
  278. if (m_avatars.TryGetValue(localID, out actor)) {
  279. actor.Collide(collidingWith, type, Vector3.Zero, Vector3.UnitZ, 0.01f);
  280. return;
  281. }
  282. return;
  283. }
  284. public override void GetResults() { }
  285. public override void SetTerrain(float[] heightMap) {
  286. m_log.DebugFormat("{0}: SetTerrain", LogHeader);
  287. m_heightMap = heightMap;
  288. this.TaintedObject(delegate()
  289. {
  290. BulletSimAPI.SetHeightmap(m_worldID, m_heightMap);
  291. });
  292. }
  293. public float GetTerrainHeightAtXY(float tX, float tY)
  294. {
  295. return m_heightMap[((int)tX) * Constants.RegionSize + ((int)tY)];
  296. }
  297. public override void SetWaterLevel(float baseheight)
  298. {
  299. m_waterLevel = baseheight;
  300. }
  301. public float GetWaterLevel()
  302. {
  303. return m_waterLevel;
  304. }
  305. public override void DeleteTerrain()
  306. {
  307. m_log.DebugFormat("{0}: DeleteTerrain()", LogHeader);
  308. }
  309. public override void Dispose()
  310. {
  311. m_log.DebugFormat("{0}: Dispose()", LogHeader);
  312. }
  313. public override Dictionary<uint, float> GetTopColliders()
  314. {
  315. return new Dictionary<uint, float>();
  316. }
  317. public override bool IsThreaded { get { return false; } }
  318. /// <summary>
  319. /// Routine to figure out if we need to mesh this prim with our mesher
  320. /// </summary>
  321. /// <param name="pbs"></param>
  322. /// <returns>true if the prim needs meshing</returns>
  323. public bool NeedsMeshing(PrimitiveBaseShape pbs)
  324. {
  325. // most of this is redundant now as the mesher will return null if it cant mesh a prim
  326. // but we still need to check for sculptie meshing being enabled so this is the most
  327. // convenient place to do it for now...
  328. // int iPropertiesNotSupportedDefault = 0;
  329. if (pbs.SculptEntry && !_meshSculptedPrim)
  330. {
  331. // m_log.DebugFormat("{0}: NeedsMeshing: scultpy mesh", LogHeader);
  332. return false;
  333. }
  334. // if it's a standard box or sphere with no cuts, hollows, twist or top shear, return false since Bullet
  335. // can use an internal representation for the prim
  336. if (!_forceSimplePrimMeshing)
  337. {
  338. // m_log.DebugFormat("{0}: NeedsMeshing: simple mesh: profshape={1}, curve={2}", LogHeader, pbs.ProfileShape, pbs.PathCurve);
  339. if ((pbs.ProfileShape == ProfileShape.Square && pbs.PathCurve == (byte)Extrusion.Straight)
  340. || (pbs.ProfileShape == ProfileShape.HalfCircle && pbs.PathCurve == (byte)Extrusion.Curve1
  341. && pbs.Scale.X == pbs.Scale.Y && pbs.Scale.Y == pbs.Scale.Z))
  342. {
  343. if (pbs.ProfileBegin == 0 && pbs.ProfileEnd == 0
  344. && pbs.ProfileHollow == 0
  345. && pbs.PathTwist == 0 && pbs.PathTwistBegin == 0
  346. && pbs.PathBegin == 0 && pbs.PathEnd == 0
  347. && pbs.PathTaperX == 0 && pbs.PathTaperY == 0
  348. && pbs.PathScaleX == 100 && pbs.PathScaleY == 100
  349. && pbs.PathShearX == 0 && pbs.PathShearY == 0)
  350. {
  351. return false;
  352. }
  353. }
  354. }
  355. /* TODO: verify that the mesher will now do all these shapes
  356. if (pbs.ProfileHollow != 0)
  357. iPropertiesNotSupportedDefault++;
  358. if ((pbs.PathBegin != 0) || pbs.PathEnd != 0)
  359. iPropertiesNotSupportedDefault++;
  360. if ((pbs.PathTwistBegin != 0) || (pbs.PathTwist != 0))
  361. iPropertiesNotSupportedDefault++;
  362. if ((pbs.ProfileBegin != 0) || pbs.ProfileEnd != 0)
  363. iPropertiesNotSupportedDefault++;
  364. if ((pbs.PathScaleX != 100) || (pbs.PathScaleY != 100))
  365. iPropertiesNotSupportedDefault++;
  366. if ((pbs.PathShearX != 0) || (pbs.PathShearY != 0))
  367. iPropertiesNotSupportedDefault++;
  368. if (pbs.ProfileShape == ProfileShape.Circle && pbs.PathCurve == (byte)Extrusion.Straight)
  369. iPropertiesNotSupportedDefault++;
  370. if (pbs.ProfileShape == ProfileShape.HalfCircle && pbs.PathCurve == (byte)Extrusion.Curve1 && (pbs.Scale.X != pbs.Scale.Y || pbs.Scale.Y != pbs.Scale.Z || pbs.Scale.Z != pbs.Scale.X))
  371. iPropertiesNotSupportedDefault++;
  372. if (pbs.ProfileShape == ProfileShape.HalfCircle && pbs.PathCurve == (byte) Extrusion.Curve1)
  373. iPropertiesNotSupportedDefault++;
  374. // test for torus
  375. if ((pbs.ProfileCurve & 0x07) == (byte)ProfileShape.Square)
  376. {
  377. if (pbs.PathCurve == (byte)Extrusion.Curve1)
  378. {
  379. iPropertiesNotSupportedDefault++;
  380. }
  381. }
  382. else if ((pbs.ProfileCurve & 0x07) == (byte)ProfileShape.Circle)
  383. {
  384. if (pbs.PathCurve == (byte)Extrusion.Straight)
  385. {
  386. iPropertiesNotSupportedDefault++;
  387. }
  388. // ProfileCurve seems to combine hole shape and profile curve so we need to only compare against the lower 3 bits
  389. else if (pbs.PathCurve == (byte)Extrusion.Curve1)
  390. {
  391. iPropertiesNotSupportedDefault++;
  392. }
  393. }
  394. else if ((pbs.ProfileCurve & 0x07) == (byte)ProfileShape.HalfCircle)
  395. {
  396. if (pbs.PathCurve == (byte)Extrusion.Curve1 || pbs.PathCurve == (byte)Extrusion.Curve2)
  397. {
  398. iPropertiesNotSupportedDefault++;
  399. }
  400. }
  401. else if ((pbs.ProfileCurve & 0x07) == (byte)ProfileShape.EquilateralTriangle)
  402. {
  403. if (pbs.PathCurve == (byte)Extrusion.Straight)
  404. {
  405. iPropertiesNotSupportedDefault++;
  406. }
  407. else if (pbs.PathCurve == (byte)Extrusion.Curve1)
  408. {
  409. iPropertiesNotSupportedDefault++;
  410. }
  411. }
  412. if (iPropertiesNotSupportedDefault == 0)
  413. {
  414. return false;
  415. }
  416. */
  417. return true;
  418. }
  419. // The calls to the PhysicsActors can't directly call into the physics engine
  420. // because it might be busy. We we delay changes to a known time.
  421. // We rely on C#'s closure to save and restore the context for the delegate.
  422. public void TaintedObject(TaintCallback callback)
  423. {
  424. lock (_taintLock)
  425. _taintedObjects.Add(callback);
  426. return;
  427. }
  428. // When someone tries to change a property on a BSPrim or BSCharacter, the object queues
  429. // a callback into itself to do the actual property change. That callback is called
  430. // here just before the physics engine is called to step the simulation.
  431. public void ProcessTaints()
  432. {
  433. if (_taintedObjects.Count > 0) // save allocating new list if there is nothing to process
  434. {
  435. // swizzle a new list into the list location so we can process what's there
  436. List<TaintCallback> oldList;
  437. lock (_taintLock)
  438. {
  439. oldList = _taintedObjects;
  440. _taintedObjects = new List<TaintCallback>();
  441. }
  442. foreach (TaintCallback callback in oldList)
  443. {
  444. try
  445. {
  446. callback();
  447. }
  448. catch (Exception e)
  449. {
  450. m_log.ErrorFormat("{0}: ProcessTaints: Exception: {1}", LogHeader, e);
  451. }
  452. }
  453. oldList.Clear();
  454. }
  455. }
  456. #region Vehicles
  457. // Make so the scene will call this prim for vehicle actions each tick.
  458. // Safe to call if prim is already in the vehicle list.
  459. public void AddVehiclePrim(BSPrim vehicle)
  460. {
  461. lock (m_vehicles)
  462. {
  463. if (!m_vehicles.Contains(vehicle))
  464. {
  465. m_vehicles.Add(vehicle);
  466. }
  467. }
  468. }
  469. // Remove a prim from our list of vehicles.
  470. // Safe to call if the prim is not in the vehicle list.
  471. public void RemoveVehiclePrim(BSPrim vehicle)
  472. {
  473. lock (m_vehicles)
  474. {
  475. if (m_vehicles.Contains(vehicle))
  476. {
  477. m_vehicles.Remove(vehicle);
  478. }
  479. }
  480. }
  481. // Some prims have extra vehicle actions
  482. // no locking because only called when physics engine is not busy
  483. private void ProcessVehicles(float timeStep)
  484. {
  485. foreach (BSPrim prim in m_vehicles)
  486. {
  487. prim.StepVehicle(timeStep);
  488. }
  489. }
  490. #endregion Vehicles
  491. }
  492. }