/* * Copyright (c) Contributors, http://opensimulator.org/ * See CONTRIBUTORS.TXT for a full list of copyright holders. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyrightD * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the OpenSimulator Project nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.Text; using OpenSim.Framework; using OpenSim.Region.Framework; using OpenSim.Region.CoreModules; using OpenSim.Region.Physics.Manager; using Nini.Config; using log4net; using OpenMetaverse; namespace OpenSim.Region.Physics.BulletSPlugin { public class BSTerrainManager { static string LogHeader = "[BULLETSIM TERRAIN MANAGER]"; // These height values are fractional so the odd values will be // noticable when debugging. public const float HEIGHT_INITIALIZATION = 24.987f; public const float HEIGHT_INITIAL_LASTHEIGHT = 24.876f; public const float HEIGHT_GETHEIGHT_RET = 24.765f; // If the min and max height are equal, we reduce the min by this // amount to make sure that a bounding box is built for the terrain. public const float HEIGHT_EQUAL_FUDGE = 0.2f; public const float TERRAIN_COLLISION_MARGIN = 0.0f; // Until the whole simulator is changed to pass us the region size, we rely on constants. public Vector3 DefaultRegionSize = new Vector3(Constants.RegionSize, Constants.RegionSize, Constants.RegionHeight); // The scene that I am part of private BSScene PhysicsScene { get; set; } // The ground plane created to keep thing from falling to infinity. private BulletBody m_groundPlane; // If doing mega-regions, if we're region zero we will be managing multiple // region terrains since region zero does the physics for the whole mega-region. private Dictionary<Vector2, BulletHeightMapInfo> m_heightMaps; // True of the terrain has been modified. // Used to force recalculation of terrain height after terrain has been modified private bool m_terrainModified; // If we are doing mega-regions, terrains are added from TERRAIN_ID to m_terrainCount. // This is incremented before assigning to new region so it is the last ID allocated. private uint m_terrainCount = BSScene.CHILDTERRAIN_ID - 1; public uint HighestTerrainID { get {return m_terrainCount; } } // If doing mega-regions, this holds our offset from region zero of // the mega-regions. "parentScene" points to the PhysicsScene of region zero. private Vector3 m_worldOffset; // If the parent region (region 0), this is the extent of the combined regions // relative to the origin of region zero private Vector3 m_worldMax; private PhysicsScene MegaRegionParentPhysicsScene { get; set; } public BSTerrainManager(BSScene physicsScene) { PhysicsScene = physicsScene; m_heightMaps = new Dictionary<Vector2,BulletHeightMapInfo>(); m_terrainModified = false; // Assume one region of default size m_worldOffset = Vector3.Zero; m_worldMax = new Vector3(DefaultRegionSize); MegaRegionParentPhysicsScene = null; } // Create the initial instance of terrain and the underlying ground plane. // The objects are allocated in the unmanaged space and the pointers are tracked // by the managed code. // The terrains and the groundPlane are not added to the list of PhysObjects. // This is called from the initialization routine so we presume it is // safe to call Bullet in real time. We hope no one is moving prims around yet. public void CreateInitialGroundPlaneAndTerrain() { // The ground plane is here to catch things that are trying to drop to negative infinity BulletShape groundPlaneShape = new BulletShape( BulletSimAPI.CreateGroundPlaneShape2(BSScene.GROUNDPLANE_ID, 1f, TERRAIN_COLLISION_MARGIN), ShapeData.PhysicsShapeType.SHAPE_GROUNDPLANE); m_groundPlane = new BulletBody(BSScene.GROUNDPLANE_ID, BulletSimAPI.CreateBodyWithDefaultMotionState2(groundPlaneShape.ptr, BSScene.GROUNDPLANE_ID, Vector3.Zero, Quaternion.Identity)); BulletSimAPI.AddObjectToWorld2(PhysicsScene.World.ptr, m_groundPlane.ptr); // Everything collides with the ground plane. BulletSimAPI.SetCollisionFilterMask2(m_groundPlane.ptr, (uint)CollisionFilterGroups.GroundPlaneFilter, (uint)CollisionFilterGroups.GroundPlaneMask); Vector3 minTerrainCoords = new Vector3(0f, 0f, HEIGHT_INITIALIZATION - HEIGHT_EQUAL_FUDGE); Vector3 maxTerrainCoords = new Vector3(DefaultRegionSize.X, DefaultRegionSize.Y, HEIGHT_INITIALIZATION); int totalHeights = (int)maxTerrainCoords.X * (int)maxTerrainCoords.Y; float[] initialMap = new float[totalHeights]; for (int ii = 0; ii < totalHeights; ii++) { initialMap[ii] = HEIGHT_INITIALIZATION; } UpdateOrCreateTerrain(BSScene.TERRAIN_ID, initialMap, minTerrainCoords, maxTerrainCoords, true); } // Release all the terrain structures we might have allocated public void ReleaseGroundPlaneAndTerrain() { if (m_groundPlane.ptr != IntPtr.Zero) { if (BulletSimAPI.RemoveObjectFromWorld2(PhysicsScene.World.ptr, m_groundPlane.ptr)) { BulletSimAPI.DestroyObject2(PhysicsScene.World.ptr, m_groundPlane.ptr); } m_groundPlane.ptr = IntPtr.Zero; } ReleaseTerrain(); } // Release all the terrain we have allocated public void ReleaseTerrain() { foreach (KeyValuePair<Vector2, BulletHeightMapInfo> kvp in m_heightMaps) { if (BulletSimAPI.RemoveObjectFromWorld2(PhysicsScene.World.ptr, kvp.Value.terrainBody.ptr)) { BulletSimAPI.DestroyObject2(PhysicsScene.World.ptr, kvp.Value.terrainBody.ptr); BulletSimAPI.ReleaseHeightMapInfo2(kvp.Value.Ptr); } } m_heightMaps.Clear(); } // The simulator wants to set a new heightmap for the terrain. public void SetTerrain(float[] heightMap) { float[] localHeightMap = heightMap; PhysicsScene.TaintedObject("TerrainManager.SetTerrain", delegate() { if (m_worldOffset != Vector3.Zero && MegaRegionParentPhysicsScene != null) { // If a child of a mega-region, we shouldn't have any terrain allocated for us ReleaseGroundPlaneAndTerrain(); // If doing the mega-prim stuff and we are the child of the zero region, // the terrain is added to our parent if (MegaRegionParentPhysicsScene is BSScene) { DetailLog("{0},SetTerrain.ToParent,offset={1},worldMax={2}", BSScene.DetailLogZero, m_worldOffset, m_worldMax); ((BSScene)MegaRegionParentPhysicsScene).TerrainManager.UpdateOrCreateTerrain(BSScene.CHILDTERRAIN_ID, localHeightMap, m_worldOffset, m_worldOffset + DefaultRegionSize, true); } } else { // If not doing the mega-prim thing, just change the terrain DetailLog("{0},SetTerrain.Existing", BSScene.DetailLogZero); UpdateOrCreateTerrain(BSScene.TERRAIN_ID, localHeightMap, m_worldOffset, m_worldOffset + DefaultRegionSize, true); } }); } // If called with no mapInfo for the terrain, this will create a new mapInfo and terrain // based on the passed information. The 'id' should be either the terrain id or // BSScene.CHILDTERRAIN_ID. If the latter, a new child terrain ID will be allocated and used. // The latter feature is for creating child terrains for mega-regions. // If called with a mapInfo in m_heightMaps but the terrain has no body yet (mapInfo.terrainBody.Ptr == 0) // then a new body and shape is created and the mapInfo is filled. // This call is used for doing the initial terrain creation. // If called with a mapInfo in m_heightMaps and there is an existing terrain body, a new // terrain shape is created and added to the body. // This call is most often used to update the heightMap and parameters of the terrain. // The 'doNow' boolean says whether to do all the unmanaged activities right now (like when // calling this routine from initialization or taint-time routines) or whether to delay // all the unmanaged activities to taint-time. private void UpdateOrCreateTerrain(uint id, float[] heightMap, Vector3 minCoords, Vector3 maxCoords, bool atTaintTime) { DetailLog("{0},BSTerrainManager.UpdateOrCreateTerrain,call,minC={1},maxC={2},atTaintTime={3}", BSScene.DetailLogZero, minCoords, maxCoords, atTaintTime); float minZ = float.MaxValue; float maxZ = float.MinValue; Vector2 terrainRegionBase = new Vector2(minCoords.X, minCoords.Y); int heightMapSize = heightMap.Length; for (int ii = 0; ii < heightMapSize; ii++) { float height = heightMap[ii]; if (height < minZ) minZ = height; if (height > maxZ) maxZ = height; } // The shape of the terrain is from its base to its extents. minCoords.Z = minZ; maxCoords.Z = maxZ; BulletHeightMapInfo mapInfo; if (m_heightMaps.TryGetValue(terrainRegionBase, out mapInfo)) { // If this is terrain we know about, it's easy to update mapInfo.heightMap = heightMap; mapInfo.minCoords = minCoords; mapInfo.maxCoords = maxCoords; mapInfo.minZ = minZ; mapInfo.maxZ = maxZ; mapInfo.sizeX = maxCoords.X - minCoords.X; mapInfo.sizeY = maxCoords.Y - minCoords.Y; DetailLog("{0},UpdateOrCreateTerrain:UpdateExisting,call,terrainBase={1},minC={2}, maxC={3}, szX={4}, szY={5}", BSScene.DetailLogZero, terrainRegionBase, mapInfo.minCoords, mapInfo.maxCoords, mapInfo.sizeX, mapInfo.sizeY); BSScene.TaintCallback rebuildOperation = delegate() { if (MegaRegionParentPhysicsScene != null) { // It's possible that Combine() was called after this code was queued. // If we are a child of combined regions, we don't create any terrain for us. DetailLog("{0},UpdateOrCreateTerrain:AmACombineChild,taint", BSScene.DetailLogZero); // Get rid of any terrain that may have been allocated for us. ReleaseGroundPlaneAndTerrain(); // I hate doing this, but just bail return; } if (mapInfo.terrainBody.ptr != IntPtr.Zero) { // Updating an existing terrain. DetailLog("{0},UpdateOrCreateTerrain:UpdateExisting,taint,terrainBase={1},minC={2}, maxC={3}, szX={4}, szY={5}", BSScene.DetailLogZero, terrainRegionBase, mapInfo.minCoords, mapInfo.maxCoords, mapInfo.sizeX, mapInfo.sizeY); // Remove from the dynamics world because we're going to mangle this object BulletSimAPI.RemoveObjectFromWorld2(PhysicsScene.World.ptr, mapInfo.terrainBody.ptr); // Get rid of the old terrain BulletSimAPI.DestroyObject2(PhysicsScene.World.ptr, mapInfo.terrainBody.ptr); BulletSimAPI.ReleaseHeightMapInfo2(mapInfo.Ptr); mapInfo.Ptr = IntPtr.Zero; /* // NOTE: This routine is half here because I can't get the terrain shape replacement // to work. In the short term, the above three lines completely delete the old // terrain and the code below recreates one from scratch. // Hopefully the Bullet community will help me out on this one. // First, release the old collision shape (there is only one terrain) BulletSimAPI.DeleteCollisionShape2(m_physicsScene.World.Ptr, mapInfo.terrainShape.Ptr); // Fill the existing height map info with the new location and size information BulletSimAPI.FillHeightMapInfo2(m_physicsScene.World.Ptr, mapInfo.Ptr, mapInfo.ID, mapInfo.minCoords, mapInfo.maxCoords, mapInfo.heightMap, TERRAIN_COLLISION_MARGIN); // Create a terrain shape based on the new info mapInfo.terrainShape = new BulletShape(BulletSimAPI.CreateTerrainShape2(mapInfo.Ptr)); // Stuff the shape into the existing terrain body BulletSimAPI.SetBodyShape2(m_physicsScene.World.Ptr, mapInfo.terrainBody.Ptr, mapInfo.terrainShape.Ptr); */ } // else { // Creating a new terrain. DetailLog("{0},UpdateOrCreateTerrain:CreateNewTerrain,taint,baseX={1},baseY={2},minZ={3},maxZ={4}", BSScene.DetailLogZero, mapInfo.minCoords.X, mapInfo.minCoords.Y, minZ, maxZ); mapInfo.ID = id; mapInfo.Ptr = BulletSimAPI.CreateHeightMapInfo2(PhysicsScene.World.ptr, mapInfo.ID, mapInfo.minCoords, mapInfo.maxCoords, mapInfo.heightMap, TERRAIN_COLLISION_MARGIN); // The terrain object initial position is at the center of the object Vector3 centerPos; centerPos.X = minCoords.X + (mapInfo.sizeX / 2f); centerPos.Y = minCoords.Y + (mapInfo.sizeY / 2f); centerPos.Z = minZ + ((maxZ - minZ) / 2f); // Create the terrain shape from the mapInfo mapInfo.terrainShape = new BulletShape(BulletSimAPI.CreateTerrainShape2(mapInfo.Ptr), ShapeData.PhysicsShapeType.SHAPE_TERRAIN); mapInfo.terrainBody = new BulletBody(mapInfo.ID, BulletSimAPI.CreateBodyWithDefaultMotionState2(mapInfo.terrainShape.ptr, id, centerPos, Quaternion.Identity)); } // Make sure the entry is in the heightmap table m_heightMaps[terrainRegionBase] = mapInfo; // Set current terrain attributes BulletSimAPI.SetFriction2(mapInfo.terrainBody.ptr, PhysicsScene.Params.terrainFriction); BulletSimAPI.SetHitFraction2(mapInfo.terrainBody.ptr, PhysicsScene.Params.terrainHitFraction); BulletSimAPI.SetRestitution2(mapInfo.terrainBody.ptr, PhysicsScene.Params.terrainRestitution); BulletSimAPI.SetCollisionFlags2(mapInfo.terrainBody.ptr, CollisionFlags.CF_STATIC_OBJECT); BulletSimAPI.SetMassProps2(mapInfo.terrainBody.ptr, 0f, Vector3.Zero); BulletSimAPI.UpdateInertiaTensor2(mapInfo.terrainBody.ptr); // Return the new terrain to the world of physical objects BulletSimAPI.AddObjectToWorld2(PhysicsScene.World.ptr, mapInfo.terrainBody.ptr); // redo its bounding box now that it is in the world BulletSimAPI.UpdateSingleAabb2(PhysicsScene.World.ptr, mapInfo.terrainBody.ptr); BulletSimAPI.SetCollisionFilterMask2(mapInfo.terrainBody.ptr, (uint)CollisionFilterGroups.TerrainFilter, (uint)CollisionFilterGroups.TerrainMask); // Make sure the new shape is processed. // BulletSimAPI.Activate2(mapInfo.terrainBody.ptr, true); BulletSimAPI.ForceActivationState2(mapInfo.terrainBody.ptr, ActivationState.DISABLE_SIMULATION); m_terrainModified = true; }; // There is the option to do the changes now (we're already in 'taint time'), or // to do the Bullet operations later. if (atTaintTime) rebuildOperation(); else PhysicsScene.TaintedObject("BSScene.UpdateOrCreateTerrain:UpdateExisting", rebuildOperation); } else { // We don't know about this terrain so either we are creating a new terrain or // our mega-prim child is giving us a new terrain to add to the phys world // if this is a child terrain, calculate a unique terrain id uint newTerrainID = id; if (newTerrainID >= BSScene.CHILDTERRAIN_ID) newTerrainID = ++m_terrainCount; float[] heightMapX = heightMap; Vector3 minCoordsX = minCoords; Vector3 maxCoordsX = maxCoords; DetailLog("{0},UpdateOrCreateTerrain:NewTerrain,call,id={1}, minC={2}, maxC={3}", BSScene.DetailLogZero, newTerrainID, minCoords, minCoords); // Code that must happen at taint-time BSScene.TaintCallback createOperation = delegate() { DetailLog("{0},UpdateOrCreateTerrain:NewTerrain,taint,baseX={1},baseY={2}", BSScene.DetailLogZero, minCoords.X, minCoords.Y); // Create a new mapInfo that will be filled with the new info mapInfo = new BulletHeightMapInfo(id, heightMapX, BulletSimAPI.CreateHeightMapInfo2(PhysicsScene.World.ptr, newTerrainID, minCoordsX, maxCoordsX, heightMapX, TERRAIN_COLLISION_MARGIN)); // Put the unfilled heightmap info into the collection of same m_heightMaps.Add(terrainRegionBase, mapInfo); // Build the terrain UpdateOrCreateTerrain(newTerrainID, heightMap, minCoords, maxCoords, true); m_terrainModified = true; }; // If already in taint-time, just call Bullet. Otherwise queue the operations for the safe time. if (atTaintTime) createOperation(); else PhysicsScene.TaintedObject("BSScene.UpdateOrCreateTerrain:NewTerrain", createOperation); } } // Someday we will have complex terrain with caves and tunnels public float GetTerrainHeightAtXYZ(Vector3 loc) { // For the moment, it's flat and convex return GetTerrainHeightAtXY(loc.X, loc.Y); } // Given an X and Y, find the height of the terrain. // Since we could be handling multiple terrains for a mega-region, // the base of the region is calcuated assuming all regions are // the same size and that is the default. // Once the heightMapInfo is found, we have all the information to // compute the offset into the array. private float lastHeightTX = 999999f; private float lastHeightTY = 999999f; private float lastHeight = HEIGHT_INITIAL_LASTHEIGHT; private float GetTerrainHeightAtXY(float tX, float tY) { // You'd be surprized at the number of times this routine is called // with the same parameters as last time. if (!m_terrainModified && lastHeightTX == tX && lastHeightTY == tY) return lastHeight; lastHeightTX = tX; lastHeightTY = tY; float ret = HEIGHT_GETHEIGHT_RET; int offsetX = ((int)(tX / (int)DefaultRegionSize.X)) * (int)DefaultRegionSize.X; int offsetY = ((int)(tY / (int)DefaultRegionSize.Y)) * (int)DefaultRegionSize.Y; Vector2 terrainBaseXY = new Vector2(offsetX, offsetY); BulletHeightMapInfo mapInfo; if (m_heightMaps.TryGetValue(terrainBaseXY, out mapInfo)) { float regionX = tX - offsetX; float regionY = tY - offsetY; int mapIndex = (int)regionY * (int)mapInfo.sizeY + (int)regionX; try { ret = mapInfo.heightMap[mapIndex]; } catch { // Sometimes they give us wonky values of X and Y. Give a warning and return something. PhysicsScene.Logger.WarnFormat("{0} Bad request for terrain height. terrainBase={1}, x={2}, y={3}", LogHeader, terrainBaseXY, regionX, regionY); ret = HEIGHT_GETHEIGHT_RET; } // DetailLog("{0},BSTerrainManager.GetTerrainHeightAtXY,bX={1},baseY={2},szX={3},szY={4},regX={5},regY={6},index={7},ht={8}", // BSScene.DetailLogZero, offsetX, offsetY, mapInfo.sizeX, mapInfo.sizeY, regionX, regionY, mapIndex, ret); } else { PhysicsScene.Logger.ErrorFormat("{0} GetTerrainHeightAtXY: terrain not found: region={1}, x={2}, y={3}", LogHeader, PhysicsScene.RegionName, tX, tY); } m_terrainModified = false; lastHeight = ret; return ret; } // Although no one seems to check this, I do support combining. public bool SupportsCombining() { return true; } // This routine is called two ways: // One with 'offset' and 'pScene' zero and null but 'extents' giving the maximum // extent of the combined regions. This is to inform the parent of the size // of the combined regions. // and one with 'offset' as the offset of the child region to the base region, // 'pScene' pointing to the parent and 'extents' of zero. This informs the // child of its relative base and new parent. public void Combine(PhysicsScene pScene, Vector3 offset, Vector3 extents) { m_worldOffset = offset; m_worldMax = extents; MegaRegionParentPhysicsScene = pScene; if (pScene != null) { // We are a child. // We want m_worldMax to be the highest coordinate of our piece of terrain. m_worldMax = offset + DefaultRegionSize; } DetailLog("{0},BSTerrainManager.Combine,offset={1},extents={2},wOffset={3},wMax={4}", BSScene.DetailLogZero, offset, extents, m_worldOffset, m_worldMax); } // Unhook all the combining that I know about. public void UnCombine(PhysicsScene pScene) { // Just like ODE, for the moment a NOP DetailLog("{0},BSTerrainManager.UnCombine", BSScene.DetailLogZero); } private void DetailLog(string msg, params Object[] args) { PhysicsScene.PhysicsLogging.Write(msg, args); } } }